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.
- Robot computer setup
- First power-on
- Motor ID and config
- Joint zeroing
- Camera calibration
- 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.