Skip to content

Bring-up

Hang the robot from the hoist, legs straight, for the whole chapter; power the 48 V bus from a current-limited bench supply until the acceptance tests say otherwise. Every command runs in control/ of the deploy repository; install it per its SETUP.md, operate per OPERATIONS.md.

  1. Robot computer setup
  2. First power-on
  3. Motor ID and config
  4. Joint zeroing
  5. Camera calibration
  6. Acceptance tests

Robot computer setup

Once, before any bus is touched. Ubuntu 22.04 on the MINISFORUM X1-470.

Item Do
CAN adapters Flash each CANable PRO V2.0 with candleLight (ElmueSoft CANable Firmware Update, button held for flash mode, adapter plugged straight into the computer)
Host sudo apt remove brltty; sudo adduser $USER netdev dialout; sudo apt install can-utils libsocketcan-dev; log out and in
Bus names One udev rule per adapter serial, below; then sudo udevadm control --reload-rules && sudo udevadm trigger and replug
IMU TransducerM TM171 in the vendor's ImuAssistant: output Composite + Status, 800 Hz, USB port, gyro / accelerometer / magnetometer on, GyroErrFilter and self-adapt filter on
Cameras librealsense2 2.58.1 (librealsense2 -utils -dev -gl -udev-rules) from librealsense.realsenseai.com; deploy pins pyrealsense2==2.58.1.10581
Control stack Build deploy/control with the CMake + vcpkg + Ninja presets
# /etc/udev/rules.d/99-candlelight.rules — one line per adapter, serial from `sudo dmesg`
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can9"     # left arm
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can21"    # right arm
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can22"    # waist, shoulder_1
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can23"    # right leg
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can24"    # left leg
SUBSYSTEM=="net", ACTION=="add", ATTRS{serial}=="<serial>", NAME="can25"    # camera gimbals
SUBSYSTEM=="net", KERNEL=="can[0-9]*", GROUP="can", MODE="0660"

First power-on

Prove that every device answers, with no motor enabled.

1Power up on the bench supply

Bench supply on the 48 V bus, current limit low; boot the computer. A supply current that climbs to the limit is a short: cut power, back to Pre-power checks.

2Inventory the USB devices

lsusb -t                  # both RealSense at 5000M (USB 3), not 480M
ip link show type can     # six CAN interfaces, still down
rs-enumerate-devices -s   # two cameras; note the serials and sides
ls -la /dev/ttyACMservo*  # both gripper driver boards

3Bring up the CAN buses

python humanoid_setup_can.py
for c in can9 can21 can22 can23 can24 can25; do echo -n "$c: "; ip -details link show $c | grep -o "can state [A-Z-]*" | head -1; done

✅ Check: all six buses ERROR-ACTIVE.

4Read every motor

python humanoid_motor_temps.py   # read-only

✅ Check: 31 rows; ID and bus match the actuator map; bus voltage equals the supply on every motor; temperatures near ambient.

5Power down

python humanoid_setup_can.py --down, shut the computer down, then cut the supply.

Motor ID and config

Property Set with Notes
CAN ID RobStride vendor tool (robstride.com/download) over the vendor's USB-CAN module (CH340), one motor at a time on the bench, before it goes into a limb RS03 and RS04 ship at ID 127; the ID table is editable in standby only
Current limit python humanoid_set_current_limit.py (default scale 0.6; type yes) Writes min(default × 0.6, 40 A) per model and saves it to the drive; values on Full specifications
Torque limit --torque-limit <ratio> on every humanoid_real_env.py run 0.05 smoke test, 0.5 joint sweep, 0.8 standing
Zero Joint zeroing

1Verify and smoke-test

python humanoid_config.py       # reads type, bus voltage, position and limits of every motor; nothing moves
# power-cycle, bring the buses up, run it again: every value must read back the same
python humanoid_test_motor.py   # 5 % torque, 0.1 rad sine on the 14 arm joints and 4 gimbal joints

✅ Check: every actuator reads back unchanged after a power cycle; the smoke test moves the arm and gimbal joints only, the right way, quietly.

Positive direction: every joint's positive rotation axis points from the actuator's output shaft towards its back; fit the actuator as the model orients it and the sign follows. The reference is the deployed MuJoCo model, robot.xml in the deploy run directory.

Joint zeroing

The zero pose is the deployed model at all joint angles zero; the control stack assumes encoder 0 is that pose. Cameras look straight ahead at zero.

1Set the zeros

Motors unpowered for movement but on the bus. Hold every joint at the model's zero pose by hand, then:

python humanoid_set_zero.py     # writes the current position of all 31 motors as zero and saves it to the drives

Do not use the vendor tool's set mechanical zero: it is lost at power-off. Any later run re-zeroes all 31 motors, the gimbals included, so redo Camera calibration afterwards.

2Verify the zeros

python humanoid_config.py --zero          # ramps every joint to zero at 10 % torque — stay clear
python humanoid_gimbal_zero_check.py      # prints where the model thinks each camera looks
python humanoid_mass_check.py             # gravity torque vs the model on shoulder_2, shoulder_3, elbow, wrist_1

✅ Check: every joint ends at the zero pose; both cameras look straight ahead; every arm residual ≤ 0.35 N·m.

Camera calibration

Four steps, in order, with both cameras streaming.

1Bind the cameras to their ports

rs-enumerate-devices -s     # serials; left camera first

Set CAMERA_SERIALS in perception/vs_site_local.py, or pass --devices <left-serial> <right-serial> to the camera server. Left camera → port 5555, right → 5556.

2Check the gimbal zero

python humanoid_gimbal_zero_check.py prints each camera's predicted azimuth and elevation at encoder zero; the left should read about 0° / 0°, the right about 180° / 0° (it faces aft). Off? Re-zero the four cam_* motors alone with humanoid_set_zero.py (comment the other joints out of motor_setup_dict for that run, then restore them).

3Calibrate the gimbal gear ratio

Hold the 40 mm tag cube in front of the camera; the tool moves that gimbal 0.4 rad yaw and 0.3 rad pitch and prints gear ≈.

python calibrate_cam_gear.py               # left gimbal
python calibrate_cam_gear.py --side right  # right gimbal

4Calibrate hand-eye

Bolt tag_cube_0 / tag_cube_1 to the left / right wrist, telemetry on port 9870.

python humanoid_monitor.py --record-handeye        # move both arms ≥ 30° and ≥ 50 mm, wrist roll included; ≥ 100 valid frames per port; Ctrl+C saves
python humanoid_handeye_calibration.py             # → calibration/camera_handeye.yaml; must print VALID
python humanoid_monitor.py --handeye-yaml calibration/camera_handeye.yaml

✅ Check: the solve prints VALID with translation residual under 20 mm and rotation under 5°.

Tag fixtures

AprilTag tag36h11, 30 mm marker with a 5 mm quiet zone per face; any rigid 40 mm cube. Files: perception/tagged_bodies/ in the deploy repository.

Fixture Use Tag IDs
tag_cube_0 Hand-eye, left wrist: 40 mm core, tags on top and four sides 582–586
tag_cube_1 Hand-eye, right wrist 577–581
grasp_cube_40mm Gear calibration, hand-held; tags on all six faces 501–506

Acceptance tests

Run in order; each adds energy. On a failure, fix the cause and re-run from that test. Robot hung, legs straight, until A11.

Test Command Pass
A1 Bus integrity humanoid_setup_can.py, then humanoid_motor_temps.py, from a cold power-up, three times Six buses ERROR-ACTIVE and 31 answers every time; no error frames
A2 CAN latency humanoid_profile_motor_latency.py (1 % torque) Every bus well inside the 5 ms motor loop
A3 Harness wiggle humanoid_wiggle_watch.py while flexing every connector, clamp and limb entry Zero dropout alarms, both arms
A4 Per-joint motion humanoid_test_motor.py (5 % torque) Arm and gimbal joints move as commanded; nothing else moves; no grinding or knocking
A5 Zero and model humanoid_config.py --zero, then humanoid_mass_check.py with real_env at --torque-limit 0.4 --grav-comp Zero pose reached; arm is static; every residual MATCH (≤ 0.35 N·m)
A6 Perception Camera calibration Ports survive a power cycle; both cameras 5000M; depth 0.1–3 m; gimbal zeros agree; hand-eye VALID
A7 Grippers humanoid_end_effector_service.py --gui → Zero Gripper per hand (finds the closed stall); then humanoid_grip_slip_test.py --side left / right with a cube in the fingers Full travel both hands; lowest no-slip hold level recorded per side
A8 Posture humanoid_static_stand.py; then humanoid_real_env.py --task <task> --torque-limit 0.8 --enable-motor true --no-use-ik --grav-comp --ee-service, still hung Poses reached smoothly; two minutes under the policy with no WATCHDOG or TORSO-TILT banner
A9 Arm transit humanoid_real_env.py … --torque-limit 0.2 --enable-motor arm --no-use-ik, then humanoid_stage_walk_test.py --arm left / right Each arm completes front → side → rear → side → front without contact; Ctrl+C returns it to the power-on pose
A10 Reach and grasp OPERATIONS.md section 2, ladder T0–T6; objects at |y| ≥ 0.15 m, z ≥ 0.09 m, 0.45–0.55 m out, turned about 45° T0–T5 pass, T6 dry run gates pass, then --execute grasps
A11 Walk humanoid_nav_step_test.py on the floor, hoist attached and slack, path clear Robot walks one velocity step and stops; Ctrl+C zeroes the command

Before A11 confirm the software stop layers: Ctrl+C zeroes the command; process death zeroes after 1 s; the gamepad seizes control; stream silence fires the nav 1 s / arm 0.5 s / gaze 2 s failsafes. Pulling the pack connector is the last stop.