Maker Β· Active Project Β· Phase 7

Technical Deep Dive: LeKiwi Arm Assembly & Checkpoint Transfer

Setup, port discovery, motor initialization, calibration, teleoperation testing, and an ACT checkpoint-transfer test β€” validating both LeKiwi-class follower arms ahead of the mobile base delivery

Goal: stand up two independent LeKiwi-class follower arms β€” set up ports, motor IDs, calibration, and teleoperation β€” then sanity-check whether an existing SO-101 policy transfers onto the new hardware, before the mobile base arrives.

Result: βœ… Both arms fully calibrated and teleoperating cleanly at 60Hz with zero over-budget ticks. ⚠️ Checkpoint-transfer test (reusing the SO-101 policy, no retraining) gave an unsatisfying grasp β€” expected, since it's a hardware-validation test, not a shortcut to a trained LeKiwi policy.

Hardware

The two new SO-101 follower arms prior to assembly

Photo of the two new SO-101 follower arms prior to assembly

Environment Setup

A separate project directory, virtual environment, and lerobot clone, kept independent from the SO101 install so version or dependency changes on one side can't affect the other:

mkdir LeKiwi
cd LeKiwi
python3 -m venv kiwi
source kiwi/bin/activate
git clone https://github.com/huggingface/lerobot.git
cd lerobot
pip install -e ".[feetech]"

Note: venv folder is named kiwi, not kiwi_env β€” confirmed. A separate dependency-install step in the notes references cd projects/lekiwi/lerobot and pip install -r docs-requirements.txt, which doesn't match the clone path above and isn't a standard LeRobot requirements file β€” still flagged as needing a check, pending confirmation of what was actually run there.

Port Discovery & Motor ID Assignment

ArmPortRole
my_kiwi_arm_1/dev/tty.usbmodem5B140288161Current LeKiwi build
my_kiwi_arm_2/dev/tty.usbmodem5B141134291Reserved β€” future XLeRobot second arm

Each bus was configured independently, motor-by-motor, via lerobot-setup-motors:

lerobot-setup-motors \
    --robot.type=so101_follower \
    --robot.port=<port>

Same ID convention applied to both arms (assigned in this order, one motor connected at a time): gripper=6, wrist_roll=5, wrist_flex=4, elbow_flex=3, shoulder_lift=2, shoulder_pan=1.

Hardware lesson carried in from assembly: the initial port-detection attempts failed with no difference detected between before/after USB states. Traced to a bad cable batch β€” 5 of 5 first-tested cables failed to carry data β€” resolved with a known-good cable borrowed from the SO101 setup. Worth treating that cable batch/supplier with suspicion going forward.

Calibration

Both arms calibrated via lerobot-calibrate. wrist_roll is intentionally excluded from the min/max sweep β€” it's a continuous-rotation joint with no natural end stops, so it doesn't get bounded calibration values the way the other five joints do.

my_kiwi_arm_1 β€” /dev/tty.usbmodem5B140288161

JointMinPosMax
shoulder_pan69720583434
shoulder_lift8758813342
elbow_flex84030543062
wrist_flex78628323293
gripper140914392929

my_kiwi_arm_2 β€” /dev/tty.usbmodem5B141134291

JointMinPosMax
shoulder_pan70120113430
shoulder_lift9909973395
elbow_flex63729312931
wrist_flex83928813201
gripper154515763079

Flag: my_kiwi_arm_2's elbow_flex shows Pos and Max identical (2931/2931), unlike every other joint on both arms where Pos sits away from both bounds. Possibly the joint was left at full extension when the recording stopped rather than a true resting position β€” worth re-running this specific joint's calibration to confirm before relying on it for training data.

Teleoperation Testing

Both arms tested independently as followers, with the SO101 leader arm (/dev/tty.usbserial-11310) driving each in turn:

Metricmy_kiwi_arm_2my_kiwi_arm_1
Duration113.0 s94.3 s
Effective cadence59.96 Hz59.97 Hz
Ticks over budget0 / 6772 (0.0%)0 / 5652 (0.0%)
Work mean / worst4.1 ms / 4.4 ms4.1 ms / 4.4 ms
Pacing headroom~12.6 ms slept/tick avg~12.6 ms slept/tick avg

Both runs held a clean 60 Hz target with zero over-budget ticks and comfortable pacing headroom β€” the loop is nowhere near saturated. The teleop step itself dominates the per-tick cost (~75% of work), well ahead of observation (~24%) and sending (<1%). No timing or hardware issues on either arm under teleoperation.

The two new SO-101 follower arms after successful testing

Photo of the two new SO-101 follower arms after successful testing

ACT Checkpoint-Transfer Test

Rather than a fresh training run, this test reused the existing so101_bgdiv_v1 checkpoint β€” the background-diversity-trained ACT policy from the SO101 line β€” directly on my_kiwi_arm_2, with no retraining, to see whether an already-trained policy transfers onto new physical hardware:

lerobot-rollout \
    --robot.type=so101_follower \
    --robot.port=/dev/tty.usbmodem5B141134291 \
    --robot.id=my_kiwi_arm_2 \
    --robot.cameras='{"main": {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30, "fourcc": "MJPG"}, "gripper": {"type": "opencv", "index_or_path": 1, "width": 320, "height": 240, "fps": 30, "fourcc": "MJPG"}}' \
    --policy.type=act \
    --policy.pretrained_path=/Users/r/Documents/Projects/lerobot/outputs/train/so101_bgdiv_v1/checkpoints/last/pretrained_model \
    --policy.input_features='{"observation.state": {"type": "STATE", "shape": [6]}, "observation.images.main": {"type": "VISUAL", "shape": [3, 480, 640]}, "observation.images.gripper": {"type": "VISUAL", "shape": [3, 240, 320]}}' \
    --policy.output_features='{"action": {"type": "ACTION", "shape": [6]}}' \
    --policy.device=mps --strategy.type=base --fps=30 --duration=20 \
    --interpolation_multiplier=5 \
    --task="Pick up the object and place it in the box"

Result: unsatisfying β€” consistent with expectations for a straight checkpoint transfer rather than a real regression. The so101_bgdiv_v1 policy was trained on a specific arm unit, workspace, and set of backgrounds; a different physical arm (even same servo model), different calibration curve, and an untested workspace/lighting setup are exactly the conditions the background-diversity work already flagged as failure-prone.

Note on the task string: this run's --task was changed to "place it in the box", differing from the original SO101 dataset's "place it in the tape roll." Since ACT has no language-conditioning pathway (confirmed earlier via source inspection during the poisoning investigation), this string is a label only β€” it has no effect on the policy's actual behavior at inference time. Worth keeping in mind so this run isn't mistaken for evidence of task generalization; it isn't testing that at all.

Key Engineering Decisions & Lessons

Bad cable batch was the real blocker, not the software. The initial "port not found" failure looked like a driver or lerobot issue but traced entirely to faulty USB cables β€” 5 of 5 first-tested units failed to carry data. Worth stress-testing new cables against a known-good port before assuming a software problem.

Checkpoint transfer is a hardware-validation test, not a shortcut to a trained LeKiwi policy. Running an existing checkpoint on new hardware is a fast, cheap way to confirm the pipeline (ports, cameras, control loop, inference) works end-to-end β€” but an unsatisfying result here says nothing about whether ACT itself will work well on LeKiwi once trained on LeKiwi-specific data. The two questions are independent.

The ACT task string is inert. Changing it between runs doesn't change behavior β€” only useful as a human-readable log label, and a reminder not to over-interpret prompt changes as functional changes on this architecture.

Current Limitations (Next Iteration Targets)

Continues from LeKiwi Base: Delivery, Defects & Wheel Motor Setup. Part of Stage 2 of the roadmap.