The AY-Robots training matrix with five policies as rows and four arms as columns, including a Koch v1.1 column
Koch v1.1DynamixelLeRobotRobot Arm SetupTeleoperation

Koch v1.1 Arm Setup: Dynamixel Bus, Wiring and LeRobot

AY-Robots ResearchAugust 23, 202617 min read

Build and configure a Koch v1.1 arm: XL330 and XL430 servos, the 5 V and 12 V rails, the LeRobot koch_follower commands, and why the dataset is identical to an SO-100.

What you need to know

  • The Koch v1.1 is the Dynamixel cousin of the SO-100: six servos, five arm joints plus a gripper, same LeRobot software, different motors and power topology.
  • Leader: six XL330-M077-T on 5 V. Follower: two XL430-W250-T on 12 V plus four XL330-M288-T on 5 V from a step-down converter, all six on one TTL data line.
  • The XL330 family is rated 3.7 to 6.0 V. Putting the follower's 12 V rail on an XL330 branch is the wiring mistake that costs real money.
  • Arm joints run Extended Position mode (4), the gripper Current-based Position mode (5), which the XL430 cannot do at all.
  • Downstream nothing changes: the dataset carries the same shoulder_pan.pos to gripper.pos keys as an SO-100 recording.

The SO-100 is the arm most LeRobot tutorials assume. The Koch v1.1 came first: a six-servo, 3D-printed, leader-follower arm built around ROBOTIS Dynamixel motors. This page covers what differs: the servo lineup, the two power rails, the operating modes LeRobot burns into motor EEPROM, and the large part of the stack that does not change.

Everything below was checked against the Koch v1.1 build repository and its wiring diagram PDF (drawn 2024-07-08 by Jess Moss for Hugging Face), the ROBOTIS e-Manual pages for the three servo models, and the LeRobot sources on main, version string 0.6.2, with the newest tagged release at v0.6.1. Where those sources disagree, and they do in one place, it is flagged.

The arm, and where it came from

The original low-cost arm is Alexander Koch's low_cost_robot from Tau Robotics, credited as such in LeRobot's class docstrings. Jess Moss rebuilt it for Hugging Face as Koch v1.1: fixed screw interferences, standardised hole sizes, no screws into plastic, a printed leader platform so the follower can reach the ground, and a replacement DC converter that removes the soldering iron and the manual voltage trim. LeRobot names both versions, so koch_follower covers each.

PartLeaderFollower
Primary servos6x XL330-M077-T (18 g each)2x XL430-W250-T at the shoulders (57.2 g each)
Secondary servosnone4x XL330-M288-T (elbow, wrist flex, wrist roll, gripper)
Horn / idler hardwareXL330 frame and idler wheel setXL330 set plus a separate XL430 idler wheel set
Controller boardWaveshare Serial Bus Servo Driver BoardWaveshare Serial Bus Servo Driver Board
Power supply5 V12 V
Voltage conversionnone12 V to 5 V reducer (EP-JY-DC5)
Repo BOM total (US / EU / UK)199 USD / 305 EUR / 222 GBP278 USD / 368 EUR / 285 GBP
Where the Koch sits on this platform

AY-Robots treats the SO-100 as the reference arm and the Koch v1.1 as a compatible one: the flows work, but the reference and demo hardware are SO-100. Undecided on hardware? Read SO-100 vs Koch v1.1 first, and the SO-100 complete guide for the reference flow this page keeps referring back to. The catalog lists the Koch at roughly 250 to 350 EUR of parts; the repo BOM sums to 305 EUR leader and 368 EUR follower because it also counts clamps, screwdrivers and cables, which its footnotes say you buy once across both arms.

Dynamixel bus versus Feetech bus

Both are serial bus arms: one three-wire chain carrying power, ground and a half-duplex TTL data line, each servo answering on its own ID. That is where the similarity ends. Protocol, SDK, model numbers and control modes all differ, which is why LeRobot keeps two bus implementations.

Koch v1.1 (Dynamixel)SO-100 / SO-101 (Feetech)
Servosxl330-m077, xl330-m288, xl430-w250sts3215 on all six joints
Wire protocol in LeRobotPROTOCOL_VERSION = 2.0DEFAULT_PROTOCOL_VERSION = 0
Python SDK extradynamixel-sdk >=3.7.31,<3.9.0feetech-servo-sdk >=1.0.0,<2.0.0
Model numbers LeRobot checks1190 / 1200 / 1060777
Encoder resolution4096 counts per revolution4096 counts per revolution
Baud rateFactory 57,600 bps, LeRobot writes 1,000,000LeRobot writes 1,000,000
Arm joint operating modeExtended Position, value 4Position, value 0 in the Feetech enum
Gripper operating modeCurrent-based Position, value 5Position, value 0, plus torque and current limits
Per-joint gains LeRobot writesPosition P/I/D on elbow_flex onlyP/I/D 16 / 0 / 32 on every motor
Power rails5 V and 12 Vsingle 7.4 V rail

Two rows deserve a second look. Operating-mode numbers are not comparable across the buses: the Dynamixel enum numbers position control 3 and extended position 4, while the Feetech enum starts at POSITION = 0 and uses 1, 2, 3 for velocity, PWM and step mode. And LeRobot drives both at 1 Mbps although Dynamixel servos ship at 57,600 bps with ID 1, so lerobot-setup-motors is not cosmetics: it writes ID and baud rate into EEPROM once per motor, and a motor you skip will not answer afterwards.

The XL430-W250-T is the hard constraint. Its e-Manual lists four operating modes, velocity, position, extended position and PWM, with no current control and no current-based position control. LeRobot's table agrees, giving it 1, 3, 4, 16 against 0, 1, 3, 4, 5, 16 for the XL330s. That is why the gripper is an XL330-M288 and the XL430s sit at the shoulder.

Two rails on one bus, and the mistake that ends a build

The leader is the simple one: six XL330-M077-T motors, one Waveshare driver board, one 5 V supply, daisy-chained. The repo notes that the board's data sheet asks for 9 to 12.6 V input, and that running it at 5 V anyway is harmless to the board and necessary, because the board's servo rail is what reaches the motors.

The follower is where the topology forks, and the driver board is the distribution point rather than a bystander. The 12 V supply goes into the board's power input. The board presents two three-pin servo headers carrying DATA, 12 V and GND: one runs to the two XL430 shoulder motors, the other feeds the step-down converter, whose 5 V output powers the four XL330s further out while the data line passes through. One bus, two power domains, boundary between joint 2 and joint 3.

The 12 V mistake

The XL330-M077-T and XL330-M288-T are rated 3.7 to 6.0 V input, recommended 5.0 V. The XL430-W250-T is rated 6.5 to 12.0 V, recommended 11.1 V. Wire an XL330 branch to the raw 12 V rail, or swap the converter input and output, and you apply exactly double the maximum rated voltage to an 18 g servo. Meter the converter output before the first XL330 goes on the chain. The same error kills the 7.4 V STS3215 servos on an SO-100, so the habit transfers.

text
12 V PSU
  |
  +--> Waveshare Serial Bus Servo Driver Board, PWR input
       (board data sheet asks for 9 to 12.6 V)
         |
         |-- servo header: DATA / 12 V / GND
         |     +--> XL430-W250-T   id 1  shoulder_pan    12 V
         |     +--> XL430-W250-T   id 2  shoulder_lift   12 V
         |
         +-- servo header: DATA / 12 V / GND
               +--> Voltage reducer  EP-JY-DC5
               |      IN+  12 V  ->  OUT+  5 V
               |      IN-  GND   ->  OUT-  GND
               |
               +--> DATA passes through; the 5 V output powers:
                      XL330-M288-T  id 3  elbow_flex   5 V
                      XL330-M288-T  id 4  wrist_flex   5 V
                      XL330-M288-T  id 5  wrist_roll   5 V
                      XL330-M288-T  id 6  gripper      5 V

One half-duplex TTL DATA line reaches all six motors, Protocol 2.0.
USB-C from the driver board to the host computer.
Leader arm: same board, one 5 V supply, six XL330-M077-T, no converter.
Follower power topology as drawn in the repo's wiring diagram

Bring-up, command by command

The commands below are the console entry points LeRobot installs on main. Anything that still calls python lerobot/scripts/control_robot.py with a --control.type flag describes an older release; that file is gone. Python 3.12 or newer is required, enforced by the package metadata.

  1. 1
    Install LeRobot with the Dynamixel SDK

    The base package is thin. You need the composite extra that maps to the CLI scripts, plus the Dynamixel motor extra. SO-100 owners install the Feetech extra instead.

    bash
    conda create -y -n lerobot python=3.12
    conda activate lerobot
    
    git clone https://github.com/huggingface/lerobot.git
    cd lerobot
    pip install -e ".[core_scripts]"
    pip install -e ".[dynamixel]"
  2. 2
    Find the two serial ports

    One port per arm. The script has you unplug a cable so it can diff the port list, which beats guessing between two identical usbmodem names. On Linux the device is usually /dev/ttyACM0; on a permission error, add your user to the dialout group.

    bash
    lerobot-find-port
    
    # Example output on macOS:
    #   ['/dev/tty.usbmodem575E0032081', '/dev/tty.usbmodem575E0031751']
    #   Remove the USB cable from your MotorsBus and press Enter when done.
  3. 3
    Write IDs and baud rate, one motor at a time

    Every motor ships as ID 1 at 57,600 bps. The script walks the joints in reverse, gripper first, and each must be the only motor on the board when you press Enter. On a Waveshare board, both jumpers go on the B channel (USB).

    bash
    lerobot-setup-motors \
        --robot.type=koch_follower \
        --robot.port=/dev/tty.usbmodem575E0031751
    
    lerobot-setup-motors \
        --teleop.type=koch_leader \
        --teleop.port=/dev/tty.usbmodem575E0032081
  4. 4
    Calibrate both arms

    Move the arm to the middle of its range, press Enter, then sweep every joint through its travel. LeRobot pins shoulder_pan and wrist_roll to 0 and 4095 rather than recording them, since those two turn freely. The calibration file is keyed by the id you pass.

    bash
    lerobot-calibrate \
        --robot.type=koch_follower \
        --robot.port=/dev/tty.usbmodem575E0031751 \
        --robot.id=my_koch_follower
    
    lerobot-calibrate \
        --teleop.type=koch_leader \
        --teleop.port=/dev/tty.usbmodem575E0032081 \
        --teleop.id=my_koch_leader
  5. 5
    Teleoperate and check the mapping

    Drive the follower from the leader with the camera feeds on screen before recording. If a joint runs backwards, the calibration is wrong, not the code.

    bash
    lerobot-teleoperate \
        --robot.type=koch_follower \
        --robot.port=/dev/tty.usbmodem575E0031751 \
        --robot.id=my_koch_follower \
        --robot.cameras="{front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
        --teleop.type=koch_leader \
        --teleop.port=/dev/tty.usbmodem575E0032081 \
        --teleop.id=my_koch_leader \
        --display_data=true
  6. 6
    Record a dataset

    Same command shape as an SO-100. Documented defaults are 60 s per episode, 60 s reset, 50 episodes. Right arrow or n moves on, left arrow or r re-records, ESC or q stops and uploads.

    bash
    lerobot-record \
        --robot.type=koch_follower \
        --robot.port=/dev/tty.usbmodem575E0031751 \
        --robot.id=my_koch_follower \
        --robot.cameras="{front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
        --teleop.type=koch_leader \
        --teleop.port=/dev/tty.usbmodem575E0032081 \
        --teleop.id=my_koch_leader \
        --dataset.repo_id=${HF_USER}/koch-pick-place \
        --dataset.num_episodes=50 \
        --dataset.episode_time_s=60 \
        --dataset.single_task="Pick up the block and drop it in the bin" \
        --display_data=true
The upstream docs contradict each other on follower power

The LeRobot Koch page tells you to connect "the usb cable from your computer and the 5V power supply to the follower arm's controller board". The build repo and its wiring diagram show the assembled follower running on 12 V, with a step-down converter for the XL330 branch. Trust the wiring diagram. A 5 V bench setup hurts nothing, but it puts the XL430 shoulder motors below their 6.5 V minimum input voltage, which is out of spec and a confusing symptom.

What LeRobot writes into the motors

Reading the two classes side by side is the fastest way to understand the arm: the follower is a mixed bus, the leader uniform. Both use the same six joint names in the same order, and both normalise the gripper to 0 to 100 and the other five to -100 to 100. One asymmetry: the follower config exposes use_degrees, which switches those five to degrees for older datasets. The leader config has no such flag.

python
# src/lerobot/robots/koch_follower/koch_follower.py
norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100
motors={
    "shoulder_pan":  Motor(1, "xl430-w250", norm_mode_body),
    "shoulder_lift": Motor(2, "xl430-w250", norm_mode_body),
    "elbow_flex":    Motor(3, "xl330-m288", norm_mode_body),
    "wrist_flex":    Motor(4, "xl330-m288", norm_mode_body),
    "wrist_roll":    Motor(5, "xl330-m288", norm_mode_body),
    "gripper":       Motor(6, "xl330-m288", MotorNormMode.RANGE_0_100),
}

# src/lerobot/teleoperators/koch_leader/koch_leader.py
motors={
    "shoulder_pan":  Motor(1, "xl330-m077", MotorNormMode.RANGE_M100_100),
    ...
    "gripper":       Motor(6, "xl330-m077", MotorNormMode.RANGE_0_100),
}
KochFollower and KochLeader motor tables, LeRobot main
What is writtenValueWhy
Operating_Mode, five arm jointsEXTENDED_POSITION (4)Joint mode caps at 0 to 4095; extended position supports 512 turns, so a servo assembled near a wrap point does not fight itself.
Operating_Mode, gripperCURRENT_POSITION (5)The follower closes onto an object without forcing; on the leader it makes the handle a trigger.
Goal_Position, leader grippergripper_open_pos, default 50.0The rest position the trigger springs back to.
Drive_Mode, leader elbow_flexINVERTED (1)The leader elbow is mirrored. Leader only; the follower stores drive_mode 0.
Position_P_Gain / I / D, follower elbow_flex1500 / 0 / 600Commented in the source as closing the gap between states and actions. That joint only.
ID and Baud_Rate, all motorssequential ids, 1,000,000 bpsWritten to EEPROM by lerobot-setup-motors, replacing the factory ID 1 at 57,600.

That elbow PID line matters for data quality. In a LeRobot dataset, action is what the leader commanded and observation.state is where the follower actually was. A soft joint pulls those apart, and a policy trained on it learns to compensate for a lag that will not exist at inference time.

What stays exactly the same: the data pipeline

This is what people get wrong when they assume a different arm means a different training path. KochFollower and the generic SOFollower behind both so100_follower and so101_follower build their features from the same comprehension over motor names, so the tensors reaching a policy are identical in shape, ordering and key naming.

python
@property
def _motors_ft(self) -> dict[str, type]:
    return {f"{motor}.pos": float for motor in self.bus.motors}

# resulting action / state keys, both arms:
#   shoulder_pan.pos, shoulder_lift.pos, elbow_flex.pos,
#   wrist_flex.pos,   wrist_roll.pos,    gripper.pos
Identical in koch_follower.py and so_follower.py
The AY-Robots SO-100 hub page, the reference arm whose recording and training flows the Koch v1.1 reuses
The SO-100 hub is the reference path. A Koch v1.1 follows the same flow, because the dataset schema is identical.

One field does change. The recorder writes robot_type from robot.name, the class attribute, not the --robot.type string you typed. A Koch recording is tagged koch_follower, while SO-100 and SO-101 recordings are both tagged so_follower, since they share one generic class. Nothing in the trainer keys off it, which is a trap: a Koch-trained policy loads and runs on an SO-100 without complaining, and it is wrong. For moving data between arms, see how to collect high-quality VLA training data.

Two ways to get from a built Koch to a working policy

  1. Assemble both arms from the repo STLs and BOM, wire the two rails, meter the converter output before any XL330 goes on the chain.
  2. Install the core_scripts and dynamixel extras in a Python 3.12 environment.
  3. lerobot-find-port, then lerobot-setup-motors twice, then lerobot-calibrate twice.
  4. lerobot-record 50 episodes with a fixed scene camera and, if you can mount one, a wrist camera.
  5. Rent or own a GPU: lerobot-train --policy.type=act, or smolvla for the pretrained route.
  6. lerobot-rollout on the follower, next to the servos, and watch what breaks.
bash
lerobot-train \
  --dataset.repo_id=${HF_USER}/koch-pick-place \
  --policy.type=act \
  --output_dir=outputs/train/act_koch \
  --job_name=act_koch \
  --policy.device=cuda \
  --policy.repo_id=${HF_USER}/act_koch

lerobot-rollout \
  --strategy.type=base \
  --policy.path=${HF_USER}/act_koch \
  --robot.type=koch_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=my_koch_follower \
  --task="Pick up the block and drop it in the bin" \
  --duration=60
The training and rollout half, done locally

The --strategy.type flag picks the rollout mode: base is autonomous with no recording, sentry records continuously, highlight keeps a ring buffer, dagger is human in the loop, episodic records with reset phases. The two underestimated costs are the hours of motor bring-up before the first episode exists, and the day lost when a v3.0 dataset meets a loader wanting v2.1.

Which policy to train on Koch data

The arm does not change the model choice. These are the five trainable policies, with the numbers that matter once you have 50 episodes and a deadline.

PolicyParamsInference per action stepGPU tierMin episodesDataset format
ACT~80 M, trained from scratch20 msRTX 4090 or any 24 GB card50LeRobot v3.0
SmolVLA~450 M245 msRTX 4090 or any 24 GB card30LeRobot v3.0
GR00T N1.7~3 B, ~40 M trained during fine-tuning152 msA100 80 GB or H100 80 GB50LeRobot v2.0 or v2.1
GR00T N1.5~3 B165 msA100 80 GB or H100 80 GB50LeRobot v2.0 or v2.1
Pi0.5~3 B, PaliGemma backbone485 msA100 80 GB or H100 80 GB50LeRobot v3.0
The AY-Robots policies page comparing the five trainable models by parameters, GPU tier, inference latency and minimum episodes
The five-policy comparison on /policies. Nothing here depends on which arm recorded the dataset.

For a first Koch policy, ACT is the default answer. It has no base model at all, which is the point: it exists only after training on your task, and at 20 ms per action step it is the fastest of the five. SmolVLA is the next step up if you want a pretrained backbone and can live with 245 ms and a 30-episode floor. Start at ACT on Koch v1.1, then SmolVLA on Koch v1.1; the GR00T N1.7 and Pi0.5 guides sit alongside.

The dataset version trap, again

A LeRobot v3.0 dataset crashes the GR00T loader and has to be converted down to v2.1 first, while ACT, SmolVLA and Pi0.5 want v3.0. The format you record today decides which models will load it; dataset rejected as v3 is the shortest route out. GR00T exposes no seed in its fine-tuning entry point, so its runs are not bit-for-bit reproducible; lerobot's default seed is 1000.

Is the Koch v1.1 the right arm for you

Building a Koch v1.1 instead of an SO-100
Advantages
  • Extended Position mode on every arm joint: a servo landing near the 0 or 4095 wrap point does not force a rebuild.
  • Current-based Position mode on the gripper: the follower closes against a current ceiling, the leader handle springs back like a trigger.
  • Dynamixel Protocol 2.0 and the vendor's SDK, pinned by LeRobot as dynamixel-sdk >=3.7.31,<3.9.0.
  • XL430 shoulders give 1.4 Nm stall torque at 11.1 V against 0.52 Nm from an XL330-M288-T at 5.0 V, so torque sits where the mass is.
  • A first-class robot and teleoperator type in LeRobot, not a community fork.
Trade-offs
  • Roughly twice the parts cost of an SO-100 for the same six-servo layout.
  • Two rails plus a converter means more wiring and a failure mode that destroys parts rather than just not working.
  • Mixed servo families mean two horn and idler sets, and no swapping a spare from wrist to shoulder.
  • A compatible arm here, not the reference one, so demo hardware and the deepest failure-mode coverage assume an SO-100.
  • The XL430 supports neither current control nor current-based position control, so the mixed lineup is forced.

Where none of this helps

Four limits are worth stating plainly, because neither the arm nor the platform fixes them.

  • Remote inference does not rescue a fast task. The loop is 20 to 485 ms per action step, and public-internet round trips on top turn a working policy into a hesitant one. Cloud inference suits slow pick-and-place; reactive motion needs the policy next to the servos. See inference latency.
  • Better hardware does not fix bad data. If the wrist camera moved between episode 12 and 30, a 3 B parameter model learns the artefact.
  • A Koch-trained policy is not an SO-100 policy. Same tensor shapes, different link geometry. Nothing errors; it just misses. See degrees of freedom.
  • Extended Position mode hides assembly errors rather than preventing them. Calibration records whatever range you sweep, so a joint mounted 90 degrees off still calibrates.
The AY-Robots failure-mode index listing common robot arm and training problems with their fixes
The failure-mode index. Most Koch bring-up problems land on the same pages as SO-100 ones.

Three pages to bookmark: arm not detected (a port permission or a Waveshare jumper on the wrong channel), servo not responding (a motor that missed its turn during setup and is still ID 1 at 57,600), and joint stops early (a calibration sweep that never reached the mechanical limit). None are Koch-specific, which is the point.

What the whole loop costs

ItemCostSource
Koch v1.1 leader arm parts199 USD / 305 EUR / 222 GBPBuild repo bill of materials
Koch v1.1 follower arm parts278 USD / 368 EUR / 285 GBPBuild repo bill of materials
Koch v1.1 parts, platform catalog~250 to 350 EURAY-Robots arm catalog
One ACT or SmolVLA run~1 to 3 USD (2 to 5 h at 0.30 to 0.60 USD/h)RTX 4090 / 24 GB spot tier
One GR00T or Pi0.5 run~4 to 12 USD (3 to 6 h at 1.20 to 2.00 USD/h)A100 / H100 spot tier
3D printingFilament only, 0.2 mm layers, ~30 % infillBuild repo print settings

The asymmetry is the useful bit. The arm is a few hundred euros once; a run is the price of a coffee. The expensive resource is recording time, so record more episodes and retrain often rather than tune hyperparameters. See pricing, /try for starting without hardware, and the dataset directory for what other recordings look like.

Ready to train on your Koch v1.1 recordings?

The training matrix pairs each of the five policies with each of the four arms, Koch v1.1 included. Every cell is a guide with the GPU tier, dataset format and trainer defaults.

Open the training matrix
Can I run a policy trained on an SO-100 dataset on a Koch v1.1?

It will load and run, because both arms expose the same six action keys with the same normalisation. It will not work well. The same normalised number maps onto different link lengths and joint travel, so it is a different physical pose. Record fresh episodes on the arm you deploy on.

Why does the follower need both a 12 V supply and a step-down converter?

Because it mixes servo families. The XL430-W250-T at the shoulders is rated 6.5 to 12.0 V, recommended 11.1 V. The XL330-M288-T further out is rated 3.7 to 6.0 V, recommended 5.0 V. The 12 V supply feeds the driver board; one servo header runs to the two XL430s, the other feeds the converter making the 5 V branch for the four XL330s. The data line is shared by all six.

Do I need a U2D2 or an OpenRB-150 for a Koch v1.1?

The build repository does not use one. Its bill of materials specifies a Waveshare Serial Bus Servo Driver Board per arm, about 10 USD or 6 EUR, over USB-C, and the repo adds a printed platform on the leader base to carry that board. Set both jumpers to the B channel so the board talks over USB. Other Dynamixel interfaces are outside what the repo documents.

My XL430 shoulder motors do not respond but the XL330s do. What is wrong?

Check the rail before the software. The XL430-W250-T has a 6.5 V minimum input voltage, so a bench setup at 5 V runs it out of spec while every XL330 on the same bus sits inside its 3.7 to 6.0 V range and answers normally. The other candidate is a motor skipped during lerobot-setup-motors, still ID 1 at 57,600 bps while the bus runs at 1,000,000.

Which LeRobot version do these commands apply to?

The LeRobot sources on main, version string 0.6.2; newest tagged release v0.6.1. Console entry points include lerobot-find-port, lerobot-setup-motors, lerobot-calibrate, lerobot-teleoperate, lerobot-record, lerobot-train and lerobot-rollout. Older tutorials call python lerobot/scripts/control_robot.py with a --control.type flag; that file is gone. Python 3.12 or newer is required.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started