Build instructions
Step-by-step assembly of a Haller robot — mechanical, electrical, cabling — from parts to first power-on.
Photos pending. Each section has the assembly steps and checkpoints in prose form; matching photos / CAD references / time-lapses are TODO.
The build path has four passes. Each pass ends at a verifiable state you can power down at without losing progress:
- Mobile base → first motor spin
- Compute → Jetson booting to a usable shell
- Perception → first LiDAR scan and first camera frame
- Arms → both SO-101s holding a stable pose under HMI control
Skipping the BOM is fine for now — parts can be ordered while you assemble in parallel — but every step below assumes that page's parts list is on the bench.
The reference robot is being built arms-first, not base-first. Both SO-101s currently sit on a static tower with no mobile base attached — Passes 2–4 are done or in progress, Pass 1 is not. The order below is the one that makes sense for a builder starting from nothing; if you are following the reference build, start at Pass 2 and treat Pass 1 as later work.
Wiring is specified elsewhere. This page covers assembly. The point-to-point power tree, fusing, grounding, E-STOP and the 11-step electrical bring-up live in wiring.md — follow that for anything carrying current, and treat it as authoritative wherever this page is briefer.
Pass 1 — Mobile base
1.1 Mount the motors
Bolt each LK-TECH MF5010 to its motor mount, then bolt the motor mounts to the chassis plates. The two drive motors should sit symmetric about the chassis centerline at the documented wheel separation (0.34 m center-to-center — matches the URDF and motor_params.yaml; any deviation means odometry will drift).
One passive caster goes at the rear, freely rotating — the layout is 3-wheel differential drive: 2 driven front wheels + 1 rear caster, not four wheels.
Checkpoint. Lift the chassis, give each wheel a spin by hand. No rubbing on the frame, free rotation, no axial play.
1.2 Wire the CAN bus
Both MF5010s share a single CAN bus.
- Connect motor A's CANH/CANL to motor B's CANH/CANL.
- Add the 120 Ω termination resistor across CANH/CANL at the far end of the bus.
- Connect the bus to the CANable2 adapter's CANH/CANL.
- Plug the CANable2 into the Jetson via USB (you'll see it later as
/dev/haller_can).
1.3 Wire battery + E-stop
Battery (+) → disconnect switch → main fuse → 20 A motor branch → motor power input. Battery (–) → motor ground, landed on the single star ground node shared with both converters and the Jetson. The motors run on the raw pack rail (20.4–25.2 V); nothing steps down for them.
The 20 A branch fuse matters: it means a motor fault clears locally instead of opening the main and dropping the arms and the Jetson at the same time.
The E-stop does not sit in this path. A 22 mm contact block breaking motor current under load pits and eventually welds, and it would also kill the Jetson's rail. Instead the button breaks a relay's ~70 mA coil supply and the relay contacts switch the load — failsafe by construction, because any break in the coil loop de-energises and opens the contact. Full scheme and rationale in wiring.md §6. The HMI's software E-STOP zeros /cmd_vel and drops arm torque, but the hard E-stop is what protects you if either rclpy or the HMI hangs.
Checkpoint. With the E-stop pressed: the switched rail reads 0 V and there is no motor motion. With it released: standby current only, no motion (no enable signal sent yet). Then pull the relay's coil wire and confirm the rail drops the same way — that is the check that actually verifies the failsafe.
1.4 First motor spin
Power up. From a USB-tethered laptop with Linux:
sudo ip link set can0 type can bitrate 1000000
sudo ip link set up can0
candump can0 # one terminal — should be silent until you poke a motorUse the can_test.py bench script in the repo root to send a single-motor command and read its encoder. Each MF5010 has a unique ID — verify both respond.
Pass 1 done once both motors spin under CAN command with the E-stop in the loop.
Pass 2 — Compute
2.1 Mount the Jetson
The Orin Nano dev kit lives on top of the chassis with line-of-sight access to: the CSI camera flex cable, the USB hub, and a power inlet from the buck converter.
Mind the airflow direction of the dev-kit fan — don't trap it under another plate.
2.2 Flash JetPack 6
Flash a microSD or NVMe per NVIDIA's JetPack guide. Create the orin user — the systemd unit files hard-code this username.
Boot, finish first-run setup, expand the rootfs, set up Wi-Fi for the initial install only — Haller's deployment puts the Jetson on its own AP afterwards.
Checkpoint. SSH into the Jetson over your local Wi-Fi. nvidia-smi (or tegrastats on Jetson) shows the GPU.
2.3 Power from the buck converter
Cut over from the bench USB-C supply to the on-board sealed buck that takes the pack's 20.4–25.2 V down to 12 V, into the dev kit's barrel jack.
The Orin Nano dev kit accepts 9–20 V on a 5.5 × 2.5 mm centre-positive barrel — it is not a 5 V board, and the pack must never reach it directly. Measure at the jack under load: 12 V, holding steady while the arms move. If it sags, the converter is undersized or the return path is too thin. See power_system.md §3 for why 12 V rather than 19 V.
Check the barrel plug before you plug it in. The Waveshare servo boards use 5.5 × 2.1 mm; the Jetson needs 2.5 mm. A 2.1 plug will physically enter a 2.5 jack, fit loosely, and arc under load.
Pass 3 — Perception
3.1 RPLIDAR mount + USB
Mount the A1M8 on the chassis centerline, high enough to clear the workbench/arms in its 360° scan. Plug into the Jetson's powered USB hub directly — some unpowered hubs can't drive the LiDAR's motor cleanly.
3.2 IMX219 CSI camera
Connect the IMX219 ribbon to the Jetson CSI slot, contacts down, lock the latch. Mount the camera looking forward at a slight downward pitch (matches camera_pitch: 0.0 and camera_height: 0.15 in the traversability config — adjust if you mount differently).
Enable the CSI in JetPack:
sudo /opt/nvidia/jetson-io/jetson-io.py
# Configure Jetson Nano CSI Connector → Camera IMX219-A → save → rebootSee Vision pipeline / Hardware setup for the rest.
3.3 USB webcams
Cameras do not go on the hub. The D455 gets its own root port — RealSense devices fail intermittently behind hubs — and each wrist camera gets its own root port too, because two 640×480@30 streams in raw YUYV need ~147 Mbps each against USB 2.0's ~280 Mbps practical ceiling. Buy wrist cameras with MJPEG and force MJPEG in the capture config. Only the kilobit-class serial devices (both arm boards, CAN, LiDAR) share the hub — see wiring.md §8.
Confirm they enumerate:
v4l2-ctl --list-devicesPass 3 done once ros2 topic echo /scan --once returns data and curl http://localhost:8000/cameras/base_front/snapshot returns a JPEG (this assumes the Mobile base bring-up and Jetson deployment have happened — you can defer to Pass 4 if you're building all hardware first).
Pass 4 — Arms
The two SO-101 arms are built from the TheRobotStudio/SO-ARM100 kit. Follow the upstream assembly guide for the mechanical build — that's the canonical reference for joint-by-joint assembly.
4.1 Configure motors before assembly
The Feetech STS3215 servos all ship with ID 1; you need 1–6 per arm. The SO-101 motor configuration step walks the IDs in reverse (gripper → shoulder_pan), one motor at a time. Do this before the arm is fully assembled — once motors are buried inside the kinematic chain, you can't easily get a single one on the bus alone.
4.2 Mount each arm
Bolt each SO-101 base to its arm mounting plate, then bolt the plate to the XLeRobot tower (or, once the base exists, to the chassis). Match the kinematic conventions you'll use in hmi/backend/config.yaml — the id: right / id: left naming should reflect which side of the chassis each arm physically sits.
Cable strain relief: the Feetech daisy-chain runs from the bus adapter board (mounted near the Jetson) through the arm's base, up through each joint to the gripper. Plan slack at each joint so the cable doesn't bind through the joint's range of motion.
4.3 Power supplies
The bus adapter only powers logic over USB — the servos need their own supply at the barrel jack. On Haller both arms are fed from one adjustable buck set to 7.4 V, distributed through a WAGO node with a 6 A fuse per arm so one arm's fault doesn't take out the other. Mount the converter near the arms and keep the 7.4 V run short and in 14 AWG: the servo floor is 6.0 V, so the whole chain has only 1.4 V of headroom.
These are 7.4 V servos. 12 V destroys all twelve of them. Before any servo is connected, set the converter into a dummy load, verify 7.4 V on a multimeter across an input sweep from 25 V down to 19 V, and lock the trimpot. Then fit the 8.0 V TVS crowbar behind its fast-blow fuse. The four-layer protection scheme and why each layer exists is in wiring.md §5.
4.4 Calibrate
Both Haller arms run as followers, so run Calibrate the follower twice, once per arm. Then verify with the HMI's calibration wizard.
The arms are mechanically symmetric, so either can instead be a leader — Bring up the leader covers that, and the left arm already carries a teleoperator calibration from its original leader bring-up. Day to day it isn't used, because teleoperation is the human-pose webcam path.
Pass 4 done once you can teleop in the HMI: pick an arm card, switch to manual, drag the shoulder_pan slider, watch the joint move.
After Pass 4
You have a Haller. Two natural next steps:
- Run it in sim first to gain confidence on workflows without risking the hardware → MuJoCo simulation.
- Deploy it as a self-starting unit so power-on Just Works → Jetson deployment.
TODO
- Per-pass assembly photos.
- CAD files for chassis plates, motor mounts, arm mounting plates.
- A reference build video / time-lapse.
- A "minimum viable Haller" path (no LiDAR, single arm) so a curious builder can validate the bring-up workflow at lower cost before committing.
Bill of materials
Parts list for a Haller — compute, mobile base, arms, perception, power, teleop — with what's confirmed, what's still to buy, and what we deliberately skip.
Mobile base (ROS 2)
The three-wheel differential-drive base under ROS 2 Jazzy — packages, bring-up, topics, and the systemd service.