CAUTION: This page documents the current 2-bus architecture (Bus 1 = both arms, IDs 1–12; Bus 2 = head + wheels, IDs 1–5). Older references describe a 3-bus layout with a separate neck/wheel bus — that wiring is superseded. For this build, follow this page.

1. Build Overview

This guide walks you through building the platform with 2× SO-101 follower arms, a neck/head assembly, an omni-wheel base on an IKEA RÅSKOG cart, and an NVIDIA Jetson compute node mounted near the neck/base region.

Fully assembled XLeRobot-Pro — dual SO-101 arms and a neck-mounted camera on a three-tier IKEA RÅSKOG cart with omni-wheels
The finished platform you're building toward — rendered from the robot's URDF.

The 2-Bus Separation

All 17 motors run on two independent buses, each carrying two daisy chains. Splitting the buses this way keeps the high-torque wheel and neck motors off the same power budget as the arms, so an arm stall cannot brown out the drive base.

BusSubsystemMotorsSerial Port
Bus 1Left arm + right arm12/dev/xle_arms
Bus 2Head + wheel base5/dev/xle_head

Recommended Build Order

  1. Print all parts (files & print settings on Components)
  2. Assemble both SO-101 arms
  3. Install wrist camera mounts and grippers
  4. Assemble cart and wheel base
  5. Assemble neck / top base / head
  6. Configure motor IDs — one motor at a time, before final assembly
  7. Mount Jetson and route wiring
  8. Final mechanical assembly and cable management
  9. Bus verification and calibration
  10. Software setup — install the stack and run the robot
STEP 6 IS THE ONE PEOPLE REGRET SKIPPING. Assigning motor IDs requires connecting exactly one motor at a time. Doing it before the arms are closed up and the cable runs are dressed takes minutes; doing it afterwards means taking the robot back apart.

2. Assembly

Overall Assembly

A complete walkthrough of the build process. Use the chapter links in the sections below to jump this video straight to that step.

A) Cart

Assemble the IKEA RÅSKOG cart first to establish the rolling structure.

B) Arms (Joints 1–5)

Assemble both SO-101 follower arms using the step-by-step build video below.

Crucial wrist/gripper order:

  1. Before attaching the gripper to joint 5, install the wrist-camera attachment to the gripper
  2. Install the gripper claw/fingers
  3. Use the follower gripper approach on both arms

This order avoids reopening the wrist stack later.

C) Neck (Top Base & Head)

Jump to the neck assembly in the video above (0:00–18:51).

D) Wheels & Wheel Base

Assemble the wheels and wheel base.

Jump to the wheel assembly in the video above (18:52–32:30).

DOUBLE-NUT ANYTHING THAT ROTATES OR CARRIES LOAD. It costs one extra nut and a few seconds, and it is much easier than diagnosing a wheel that fell off mid-run.

E) Integrating Components onto the Cart

Bring the arms, neck, and wheel base together on the cart for the full platform.

Jump to the full cart integration in the video above (32:35–39:52).

3. Motor Configuration & IDs

Configure motors before final cable management and hard mounting — verifying IDs on an assembled chain is far slower. Many servos ship with default ID 1, so each ID must be unique per bus and the baudrate must match the board and motors.

DUPLICATE IDs DISABLE THE ENTIRE BUS. Feetech is a half-duplex bus — one shared data wire. Two motors answering to the same ID transmit at the same instant, and the collision corrupts every packet on the wire, not just theirs. One duplicate makes all seventeen motors scan as absent.

The signature is unmistakable once you know it: each motor responds fine in isolation, and the assembled chain responds to nothing. This reads exactly like a power fault, a dead adapter, or bad cabling — and it is none of those. A right arm shipped as IDs 1–6 duplicates the entire left arm.
Physical labels are not the programmed ID. Number stickers on the servos record intent, not state. Verify every motor electrically before you trust the chain.
USE RAW PORT NAMES HERE — /dev/ttyACM0, NOT /dev/xle_arms. The friendly bus names are created later, in software setup step 7, by a script that identifies each adapter from the motor IDs that answer on it. Those IDs are what you are assigning right now, so the names cannot exist yet. Everything in this section uses /dev/ttyACM0 (macOS: /dev/tty.usbmodem*).

Before you start

You need the software stack installed to run these tools, but nothing beyond the base install — steps 3–5 of software setup is enough. Grant serial access first, or every command below fails with Permission denied:

Serial permissions (Linux, once)
$ sudo usermod -aG dialout $USER
# log out and back in for this to take effect

# just for this boot, if you cannot log out right now:
$ sudo chmod 666 /dev/ttyACM0

ID Planning

BusRecommended IDsMapping Notes
Bus 1 (Both Arms)1–121–6 left arm, 7–12 right arm — SO-101 follower mapping, shoulder to gripper
Bus 2 (Head & Wheels)1–5ID 1 = tilt (up/down), ID 2 = pan (left/right), 3–5 wheels

Step 1 — Scan what is actually on the bus

This sweeps every supported baud rate and pings every address, so it finds motors even at the wrong baud or the wrong ID. Model 777 is the STS3215.

Scan a Bus
# Which serial ports exist right now?
$ ls /dev/ttyACM*

# List every motor answering on a bus, at any baud rate
$ python -c "from lerobot.motors.feetech import FeetechMotorsBus; \
  print(FeetechMotorsBus.scan_port('/dev/ttyACM0'))"

A brand-new motor answers as ID 1. That is why they must be configured one at a time — a bag of factory servos is seventeen motors all claiming the same address.

Step 2 — Write one ID at a time

ONE MOTOR ON THE CHAIN, NO EXCEPTIONS. set_motor_id.py reprograms whatever answers. With other motors attached it will either collide or silently rewrite a working joint — turning one problem into two.
Assign Motor IDs
# Identify active serial ports
$ lerobot-find-port

# ONE motor connected. Assign its ID from the table above.
$ python set_motor_id.py --port /dev/ttyACM0 --id 7

# Non-STS3215 motor? name the model explicitly:
$ python set_motor_id.py --port /dev/ttyACM0 --id 7 --model scs0009

The tool scans every baudrate to find the motor, so it works even on a servo that is at an unexpected ID or baud. It reprograms it to your chosen ID at the default 1 Mbps. --model defaults to sts3215, which is what this build uses throughout.

Isolate each motor, confirm its current ID with a scan, then write the correct one. Add motors back and re-scan as you go — a complete chain should list every ID with no gaps.

ASSIGN THE HIGH IDs FIRST. Every unconfigured motor answers as ID 1. If you rebuild the chain as you go, working downward (12, 11, 10 …) means no configured motor is sitting at ID 1 while unconfigured ones are still to come — so a stray reconnection cannot create a duplicate. Assigning upward guarantees a period where it can.
TIP: Physically label every motor and both ends of every cable after configuration — L-1L-6, R-7R-12, H-1H-2, W-3W-5.
Bisecting a bad chain. If an assembled chain scans empty, halve it: connect the first half and scan, then the other half. Roughly four rounds isolates the offending motor or cable. A scan that returns fewer IDs than motors attached names the culprit outright — it's the missing number.

4. NVIDIA Jetson Placement

Mount the Jetson in the center rack of the cart, between the wheel base and the top plate.

PLUG BOTH BUS ADAPTERS DIRECTLY INTO THE JETSON. Not through a hub. USB-serial adapters behind two hub levels — especially sharing a tree with the RealSense D435 — fail enumeration with error -110 and unable to enumerate USB device. The adapter looks physically connected but never creates a /dev/ttyACM* node, so its bus is invisible rather than broken. That is five USB devices on the Jetson: two adapters and three cameras. Plan the port budget around it — which is also why the software guide has you work over SSH rather than plugging in a keyboard.

5. Wiring (2-Bus)

Seventeen motors, four daisy chains, two bus hubs, five USB cables. The diagram below is the complete reference — chain order per limb, which chain lands on which hub, and how the hubs and cameras reach the Jetson. Motor positions are taken straight from the robot's URDF.

Complete XLeRobot-Pro wiring guide: 17 motors in four daisy chains — left arm IDs 1-6, right arm IDs 7-12, head IDs 1-2, wheels IDs 3-5. Arm chains join Bus 1 hub, head and wheel chains join Bus 2 hub, each hub reaching the Jetson through one USB adapter. The three cameras connect directly to the Jetson, and a 12 V battery feeds both hubs and the computer.
Full wiring reference — click to open at full size. Each motor has two identical sockets, so either one can be "in" or "out"; the last motor in every chain leaves one socket empty.

A) Motor Wiring

Four daisy chains. One cable carries both power and the control signal, which is why each motor needs only the single wire to its neighbour. The free socket on the last motor of each chain goes to its bus hub.

CONFIRM THE HEAD AXES ON YOUR HARDWARE: power the head and check that ID 1 moves up/down and ID 2 moves left/right. The vision demo reads live head tilt and pan to place the camera relative to the arm base — swapping them produces a coordinate transform that looks entirely plausible and sends the arm confidently to the wrong place. Verify before trusting a grasp.

Cable Routing

A daisy chain breaks at any point and everything downstream goes silent. Two rules prevent most of the grief:

Also: avoid tight bends near connectors, keep motion cables away from pinch points, and label both ends of every extension.

SOURCING LONG SERVO CABLES IN THE US. The long 3-pin daisy-chain cables are hard to buy domestically — most US listings stock only the short lengths that ship with the servos, and the long runs the wheel base and neck need are typically import-only. Plan on cutting and crimping your own from bulk 3-conductor servo wire, which is why the BOM lists wire rather than finished cables. Make them slightly longer than you think you need — the service loops above eat more length than expected.

B) Power Wiring

There are three separate things here and confusing them costs hours: servo power, Jetson power, and USB. USB carries data only.

RailFeedsRequirement
ComputeJetson carrierBarrel jack or USB-C PD, per NVIDIA spec
Servo — armsBus 1, 12 motors12 V, several amps — the largest aggregate load on the robot
Servo — head/baseBus 2, 5 motors12 V
Adapter boardsBus transceiversBarrel jack on each board
A 5 V USB power bank will not run these servos. STS3215s want 6–12 V. Off a 5 V USB output they respond intermittently or not at all — while the adapter board's LED glows normally on a few milliamps, so everything looks fine. If you power from a bank, take 12 V from its car/DC socket, never a USB-to-barrel cable.
A lit LED proves less than you think. The STS3215 has no external LED. Any light you see belongs to the adapter board, and it proves only that the board's barrel jack has voltage — not that the servos are powered, and not that the board is actually joined to the motor chain. The reliable test is a bus scan or a multimeter across the servo rail.

Grounding

If the adapter board and the motor chain are fed from different supplies, they must still share a common ground — otherwise the data line has no valid voltage reference. The symptom is distinctive: every motor powered and drawing current, and zero communication.

Watch for load asymmetry when diagnosing. A marginal supply often carries three wheels fine and cannot carry twelve arm joints, which presents as "the arm bus is broken."

C) Camera Wiring

The cameras do not touch the motor buses at all — each one runs its own cable into its own USB port on the Jetson, never through a bus hub.

That accounts for all five USB cables: two bus adapters and three cameras.

6. Final Assembly

Complete final assembly, substituting the Jetson where older instructions reference a Raspberry Pi.

CAUTION: Do not push the fully assembled robot like a normal cart — lift it (~12 kg) to relocate to protect the motor gears.

Final Checklist

Mechanical & Power

  • Jetson mounted securely
  • Battery secured and upright
  • No cable pinch points
  • Full arm range without cable snagging
  • Wheel base rotates freely

Electrical & Sensing

  • Both buses configured and tested independently
  • Motor IDs verified — 1–12 on Bus 1, 1–5 on Bus 2
  • Motor directions verified
  • Head axes confirmed: ID 1 up/down, ID 2 left/right
  • Both wrist cameras detected
  • Head camera detected

7. Hardware Calibration & Validation

One whole-robot calibration serves both the robot class and the vision stack. Run it first, verify it, then work through the sanity checks below.

THREE THINGS MUST BE TRUE BEFORE YOU CALIBRATE. The software stack is installed (software steps 3–6), the bus symlinks /dev/xle_arms and /dev/xle_head exist (step 7), and every motor answers a bus scan (step 8). Calibrating a chain with a missing or duplicated motor produces a file that looks fine and is wrong.

A) Run the Calibration

lerobot-calibrate cannot see this robot. Its --robot.type does not offer xlerobot_new_wiring. The robot classes are registered, but robots/utils.py imports them lazily inside its dispatch function, so the config module never loads and the registration decorator never runs. Drive the class directly with the script below instead.
calibrate_xlerobot.py
from lerobot.robots.xlerobot.config_xlerobot import XLerobotNewWiringConfig
from lerobot.robots.xlerobot.xlerobot_new_wiring import XLerobotNewWiring

config = XLerobotNewWiringConfig(
    id="xlerobot", port1="/dev/xle_arms", port2="/dev/xle_head"
)
robot = XLerobotNewWiring(config)
robot.connect(calibrate=False)
try:
    robot.calibrate()
finally:
    robot.disconnect()
TORQUE IS RELEASED FOR THE WHOLE PROCEDURE. Both arms go limp and will fall from a raised pose. Support them by hand before you start, and clear anything underneath them.

The run has four prompts: centre both arms, sweep every arm joint through its full range, centre the head, and then the wheels are handled automatically as full-turn motors.

B) Verify the Calibration — Do Not Skip This

Calibration can complete successfully and still be wrong. There are two failure modes and both are silent:

The strongest check is left/right symmetry. Mirrored joints should agree within a few percent — here is a known-good run:

Joint PairLeft SpanRight SpanRatio
shoulder_pan271527320.99
shoulder_lift241824330.99
elbow_flex221721970.99
wrist_flex235823350.99
gripper152614480.95

Two independently-swept arms landing within a few percent of each other is not something you get by accident — it is the best available evidence the calibration is sound.

C) Read Back What the Robot Thinks It Is

These print calibrated positions, so a joint reporting a wild angle at a pose you can see means the calibration did not take. Nothing is commanded to move — but read the warning first.

read_motors.py RELEASES ARM TORQUE ON PURPOSE — it is designed so you can pose the arm by hand — and then waits at a prompt. A raised arm will fall the moment it runs. Lower both arms, or hold them, before starting it.
Read calibrated positions
$ python diagnostics/read_motors.py
# arm joint angles, from /dev/xle_arms

$ python diagnostics/read_head.py
# head tilt and pan, from /dev/xle_head

D) First Commanded Motion

Start with the base — it is the only subsystem where a mistake cannot drop an arm on your hand. Then move the arms.

Wheels first, then arms
# wheels only — arms are never touched. A slow few centimetres.
$ python diagnostics/base_drive_check.py --vx 0.05 --seconds 2

# whole robot: both arms, head and base, on a gamepad
$ python examples/5_xlerobot_teleop_xbox_new_wiring.py
USE THE _new_wiring VARIANT. The plain 5_xlerobot_teleop_xbox.py targets the older three-bus layout and opens /dev/xle_right and /dev/xle_left — ports that do not exist on this build. For single-arm keyboard control, prefer the copies under examples/xlerobot_examples/, which prompt for a port, over the same-named files directly under examples/, which hardcode the old names. Full detail in software setup step 11.

E) Mechanical IK Accuracy

Once the arm takes end-effector commands, check that the gripper actually arrives where it was told. Expect 1–2 cm; worse than that is mechanical, not software — look for loose servo horns, a stripped gear, or a bent bracket. The First Reach tutorial walks through this end to end.

Compare commanded vs. actual
# pose the gripper by hand on a known point;
# prints joint angles and the model's EE position for them
$ python diagnostics/measure_ee.py

measure_ee.py hardcodes PORT = "/dev/ttyACM0" near the top of the file — change it to /dev/xle_arms for this build.

F) Camera Bring-up

All three cameras
$ v4l2-ctl --list-devices
$ python examples/9_dual_wrist_camera.py
$ rs-enumerate-devices

Hardware Is Validated When

  • Every arm joint moves in the expected direction
  • Head tilt and pan move on the expected axes
  • Wheels drive and stop reliably
  • All three camera streams open
  • Mirrored joints agree within a few percent

Then Go To

Hardware validated? Move on to software configuration.

Next Step: Software Setup ↗