Chassis geometry and weight distribution
First-principles sizing of the three-wheel base — where the arms, the battery, the caster and the camera mast go so that a fully extended arm can't tip the robot over.
This is a paper design, not a build log. Nothing on this page has been cut, printed or bolted. It fixes the numbers that the rest of the mechanical work depends on — longitudinal length, caster position, battery position, mast height — so that the first chassis is sized against arithmetic rather than against a guess. Every figure is either traced to a datasheet or flagged as an assumption.
The base is a triangle seen from above: two driven wheels on the motor axis line at the front, one swivel caster behind. The two SO-101 arms sit on a line parallel to the motor axis, and they reach forward — out past the front wheels, over the only tipping edge that matters. The whole design problem is that one sentence.
Frame and sign conventions
Origin on the ground at the midpoint of the drive-wheel axle.
- x — forward, perpendicular to the motor axis. Positive is the direction the arms reach.
- y — left, along the motor axis.
- z — up.
So the forward tipping edge is the line x = 0, and the whole of the weight-distribution problem is "keep the centre of mass at negative x".
What is already fixed
These are not design choices left to make — they are already in the repo or in a datasheet, and the rest of the geometry has to live with them.
| Quantity | Value | Where it comes from |
|---|---|---|
| Wheel track | 340 mm | motor_params.yaml + URDF. Changing it breaks odometry. |
| Wheel radius | 50 mm (⌀100) | Same. |
| Drive motors | 2× MF5010, 0.26 N·m rated / 0.40 N·m peak (10T), 137 g, ⌀49 mm | MF5010_Specs.pdf. Direct drive, no gearbox. |
| Motor bearing rated load | 153 N | Same. The wheel cantilevers off the rotor, so this is a real limit. |
| Arm separation | 266 mm | XLeRobot arm_base_joint at y = ±0.133 m in xlerobot.urdf. |
| Shoulder torque | 1.863 N·m stall | STS3215 C001, 19 kg·cm at 7.4 V. One servo per joint — no doubling. |
| Battery | Hilti B 22-195 Nuron, 1.33 kg | power_system.md. |
The sim currently places the two arms at ±200 mm (sim/builder.py, "so arms
don't overlap"). That is an arbitrary number, not the XLeRobot one. If the real
base is built at 266 mm, the sim offsets should move to ±133 mm or every policy
trained in sim inherits a 34 mm per-side geometry error.
Mass budget
Only the parts that matter for balance. Everything else — LiDAR, converters, wiring, fuses, hub — is deliberately excluded, and gets a reserve line instead.
| Item | Qty | Unit | Total | Source |
|---|---|---|---|---|
| SO-101 arm, moving links | 2 | 609 g | 1218 g | Sum of <inertial> masses in so_arm100.xml |
| SO-101 base link (servo + printed shell) | 2 | ~150 g | 300 g | 55 g STS3215 + estimated shell — assumption |
| MF5010 motor | 2 | 137 g | 274 g | Datasheet |
| Drive wheel, ⌀100 metal hub + neoprene | 2 | ~300 g | 600 g | Vendor listings — weigh these |
| Swivel caster | 1 | 200 g | 200 g | Given |
| Hilti B 22-195 Nuron | 1 | 1330 g | 1330 g | power_system.md |
| Jetson Orin Nano dev kit | 1 | 176 g | 176 g | NVIDIA |
| RealSense D455 | 1 | 103 g | 103 g | Intel community measurement (Intel publishes no figure) |
| Mast + pedestal + arm plate | 1 | ~600 g | 600 g | Printed — assumption |
| Chassis plate + motor mounts | 1 | ~1500 g | 1500 g | Design allowance — assumption |
| Dry total | 6.30 kg | |||
| Payload, 2 × 500 g | 1.00 kg | Design case | ||
| Loaded total | 7.30 kg |
Four of those eleven lines are assumptions, and they are 3.0 kg of the 6.3 —
so treat the absolute numbers as provisional and the ratios as the real
output. hardware_inventory.md has recorded "total robot mass is not measured"
as an open item for months; this table is the first estimate to close against.
What the arms can actually lift
This sets the disturbance the base has to survive, so it comes first.
The shoulder (Pitch) is a single STS3215 at 1.863 N·m stall. Holding the
arm out horizontally already costs most of that: at full extension the unloaded
gravity torque at the shoulder is 0.956 N·m, just over half the budget,
before anything is in the gripper.
Solving τ_shoulder(m_payload) ≤ 1.863 N·m across the workspace, using the MJCF
link masses and centres of mass:
| Reach from arm mount | Payload at stall | Realisable? |
|---|---|---|
| 150 mm | 2643 g | ✗ torque says yes, the gripper and wrist say no |
| 200 mm | 1510 g | ✗ |
| 250 mm | 1020 g | ~ |
| 300 mm | 731 g | ✓ |
| 350 mm | 546 g | ✓ |
| 400 mm | 403 g | ✓ |
| 450 mm | 290 g | ✓ |
| 476 mm (max reach) | 231 g | ✓ |
Max reach is 476 mm from the arm mount to the grip point, at 120 mm above the mount plane. At 50% of stall — a sane continuous rating — the payload at full extension is zero: the arm can just barely hold itself up.
The useful result: the forward moment an arm can apply is capped, and the cap
barely moves with the payload assumption. Because the shoulder axis is
parallel to the tipping edge, the arm's moment about that edge is
τ_shoulder + g · x_mount · (m_arm + m_payload), and τ_shoulder can never
exceed stall. Searching every pose:
- payload capped at 250 g → worst moment about the arm mount is 2.50 N·m
- payload capped at 500 g → worst moment is 2.69 N·m
Doubling the assumed payload moves the design load by 7%, because a heavier payload forces a shorter reach. The base can be sized against a torque, not against a workspace — which means this analysis survives changing the gripper.
Tipping: the only equation that matters
The support polygon is the triangle with vertices at the two wheel contacts
(0, ±170) and the caster contact (−L, 0).
For forward tip-over the edge is the front axle, and the caster's position is irrelevant — only the centre of mass matters:
tips forward ⟺ x_cm > 0Write d = −x_cm, the distance the CoM sits behind the axle. Then the
static load split follows from one moment balance about the front contact line:
N_caster = W · d / L N_drive = W · (1 − d / L)That is the whole trade in two symbols. d buys tip-over safety; d/L costs
traction. Push the CoM back to stop the arms tipping the robot and you unload
the drive wheels; the only way to have both is to make L — the longitudinal
length — longer.
Two more disturbances eat into d, and both scale with CoM height:
braking: d_used = a · z_cm / g slope θ: d_used = z_cm · tan θThis is where the arm mount height bites, and it is why the mast is not free.
Where the arms have to go
Run the CoM sum for the worst arm pose while sliding the arm mount line along x (battery and caster fixed):
| Arm line vs axle | d | Caster load | Tips at |
|---|---|---|---|
| +50 mm (ahead) | −3 mm | — | already over |
| 0 (on the axle) | 19 mm | 6% | 2.9° |
| −100 mm | 63 mm | 21% | 9.7° |
| −180 mm | 104 mm | 29% | 17.7° |
| −250 mm | 129 mm | 43% | 19.3° |
Mounting the arms over the wheel axle — the intuitive choice — leaves 19 mm of margin and a robot that tips at 2.9°. Mounting them ahead of the axle tips it standing still. The arms have to sit ~180 mm behind the motor axis, reaching forward over the drive wheels rather than out beyond a cantilever.
Moving the battery cannot substitute for this. To reach d = 90 mm with the
arms on the axle, the 1.33 kg pack would have to sit 600 mm behind the axle —
well outside any sane footprint. The battery is a 1.33 kg trim adjustment on a
7.3 kg robot; the arm position is the actual control.
The design point
| Parameter | Value | Why |
|---|---|---|
| Wheel track | 340 mm | fixed |
| Arm separation | 266 mm | XLeRobot |
| Arm mount line | 180 mm behind the axle | tipping — see above |
| Arm plate height | 450 mm | height trade — see below |
| Caster | 360 mm behind the axle (L) | caster load ≤ 30% loaded |
| Battery centre | 200 mm behind the axle, on the deck | trims d, keeps z_cm low |
| Mast | 300 mm behind the axle | behind the arms, over the caster |
| Camera | 950 mm above ground, 57° down | D455 min-Z — see below |
| Deck | 460 × 300 mm at 100 mm | clears wheels, matches caster height |
| Overall | 448 × 365 mm, ~1.0 m tall |
Which gives:
| Stowed | Arms extended + 2 × 500 g | |
|---|---|---|
| Mass | 6.30 kg | 7.30 kg |
CoM behind axle (d) | 167 mm | 104 mm |
| CoM height | 278 mm | 325 mm |
| Caster load | 46.3% | 28.9% |
| Tips forward at | 31.3° | 17.7° |
| Tips sideways at | 16.3° | 18.6° |
| Tips under braking at | 5.97 m/s² | 3.14 m/s² |
Forward and sideways tip angles land within 1° of each other in the loaded case
(17.7° vs 18.6°), which is the sign that L is correctly matched to the track —
neither axis is the weak one.
The margin budget
104 mm of static margin sounds generous. It is almost entirely spoken for:
Braking at the motors' peak thrust costs 72.6 mm; a 5° downslope costs another 28.4 mm. Doing both at once leaves 3 mm. The robot is stable, but it is stable by design, not by accident — which means two rules follow:
- Cap deceleration in software, or accept that a hard stop on a wheelchair ramp is the tip-over case. The limit falls from 3.14 m/s² on the flat to 2.27 m/s² on a 5° downslope — below the motors' 2.19 m/s² peak by only 4%.
- Any mass added high up spends this budget. Every 10 mm of
z_cmcosts about 3 mm of margin.
Arm mount height is stability-limited
d doesn't change with arm height, but z_cm does, and the braking term scales
with it:
| Arm plate height | z_cm | Tips at | Braking limit | vs motor peak 2.19 m/s² |
|---|---|---|---|---|
| 350 mm | 281 mm | 20.7° | 3.70 m/s² | 1.69× |
| 450 mm | 325 mm | 17.7° | 3.14 m/s² | 1.43× |
| 550 mm | 369 mm | 16.0° | 2.82 m/s² | 1.29× |
| 650 mm | 413 mm | 14.4° | 2.52 m/s² | 1.15× |
| 750 mm | 458 mm | 13.1° | 2.28 m/s² | 1.04× |
The arms cannot go to table height on this chassis. At a 750 mm arm plate —
roughly where XLeRobot puts its arms, on a 760 mm cart top — the robot tips
within 4% of the deceleration its own motors can produce. Working at 450 mm means
Haller works on low surfaces, its own bench, or the floor. Raising it to bench
height needs one of: low ballast at the back, a longer L, a wider track, or a
hard software cap on deceleration. That is a real constraint, and it should be
decided before anything is printed.
Traction and the caster load
Direct drive means thrust comes straight from motor torque with no gearbox
multiplication — F = 2τ/r:
| Torque | Thrust | Accel at 7.3 kg | Grade | |
|---|---|---|---|---|
| Rated | 0.26 N·m | 10.4 N | 1.42 m/s² | 8.4° |
| Peak (10T) | 0.40 N·m | 16.0 N | 2.19 m/s² | 12.9° |
| Peak (35T) | 0.53 N·m | 21.2 N | 2.90 m/s² | 17.2° |
Available traction is μ · N_drive:
| Surface (μ) | Loaded (50.9 N on drives) | Stowed (33.2 N on drives) |
|---|---|---|
| 0.4 — polished concrete | 20.4 N ✓ | 13.3 N — slips at peak |
| 0.5 | 25.5 N ✓ | 16.6 N ✓ |
| 0.6 — typical rubber on floor | 30.5 N ✓ | 19.9 N ✓ |
The awkward case is stowed: folding the arms back moves the CoM to 167 mm behind the axle and hands 46% of the weight to the caster. On slick floors the wheels break traction before the motors stall. That is a benign failure — slip, not fall — and current-limiting fixes it, but it is worth knowing that the robot has less grip when it is not carrying anything.
Per-wheel load peaks at ~25 N against the MF5010's 153 N bearing rating, so the cantilevered wheel mount is not a concern.
Caster swivel shrinks the support triangle. A swivel caster's contact point
trails its pivot. Right after a direction reversal the contact swings through
2 × trail before it settles — with 40 mm of trail, L is momentarily 280 mm
rather than 360 mm, pushing the caster load from 29% to 37% and the sideways tip
angle from 18.6° down to about 15°. Size L against L − 2 × trail, and prefer
a short-trail caster.
Where the camera mast goes
The D455 sits behind the arms so its mass helps the balance instead of hurting it, and so it looks over the arms rather than through them. The mast at 300 mm behind the axle puts it 120 mm behind the arm line.
The constraint that sets its height is not field of view — it is minimum depth range. The D455's wide 95 mm baseline buys it 20 m of range at the cost of a min-Z around 0.52 m; anything nearer returns no depth. The nearest point of the workspace is only 150 mm forward of the mast horizontally, so:
√(0.15² + Δz²) ≥ 0.52 → Δz ≥ 0.50 mThe camera must sit 500 mm above the arm plane — 950 mm above ground — purely so the near edge of the workspace is outside its blind zone. Field of view is not close to binding: the workspace spans 32.6° vertically against the D455's 58°, and 300 mm of half-width against the 745 mm it sees at the far edge. Depression angles run 73° (near) to 41° (far), so the mount wants ~57° down.
For comparison, XLeRobot puts its head camera 438 mm above its arm plane. Same ballpark, arrived at independently.
Ride height and caster sizing
The deck sits at 100 mm, which is set by the caster, not by the wheels: a 3-inch swivel caster with a top plate is ~100 mm from floor to mounting face. The drive axle has to be at 50 mm — the wheel radius — so the motors hang 50 mm below the deck on drop brackets. Get this wrong and the robot rocks on two wheels.
The BOM already has 4× 3-inch swivel casters on hand for a layout that needs one, so there is nothing to buy here.
What to measure before cutting anything
- Weigh the two drive wheels. 300 g each is a vendor listing, and 600 g is 10% of the dry mass.
- Weigh an assembled SO-101. The 760 g figure is 609 g of MJCF inertials plus a 150 g guess for the base link.
- Measure the XLeRobot plate's actual arm spacing and confirm the 266 mm from the URDF matches the physical part.
- Measure the caster's trail — it sets how much of
Lis real. - Confirm the MF5010 winding (10T vs 35T). It changes peak thrust by a third, which changes the braking term, which is 70% of the stability budget. This is already flagged unrecorded in the BOM.
- Decide the arm height before printing the pedestal. 450 mm and 750 mm are different robots.
Then fix the sim: move the arm offsets from ±200 mm to ±133 mm, and add the base so that policies see the same geometry they will be deployed onto.