
Joint limits, torque caps, per-step motion caps and a stop you can reach. What LeRobot enforces on an SO-100, what the Feetech firmware catches, and what nobody enforces.
A trained policy does not know what a table edge is. It emits joint targets and the arm follows them. When the policy is right this looks like competence. When it is confused, because a camera moved 3 cm or you picked the wrong checkpoint, it looks the same from the outside, right up to the moment a joint drives into its own bracket and starts pulling stall current.
The safety layer decides which of those two mornings costs you a servo. This page covers it on an SO-100 class arm: what LeRobot enforces, what the Feetech firmware enforces, what nobody enforces, and how to wire a stop you can reach. Code details are read out of lerobot main on 24 August 2026 (release v0.6.1); servo numbers come from the Feetech STS3215 product specification for the 7.4 V, 19 kg.cm model, edition A/0, 10 April 2020.
What you need to know
- •LeRobot's per-step motion cap, max_relative_target, defaults to None on the SO-100 and SO-101 follower. Until you set it, nothing limits how far one action may jump.
- •The cap is per control tick, not per second. Five degrees per tick is about 250 deg/s under ACT at 20 ms per action step and about 10 deg/s under Pi0.5 at 485 ms. Same number, very different arm.
- •Joint limits do live in hardware: calibration writes your swept range into the servo's Min_Position_Limit and Max_Position_Limit EEPROM registers, where it survives a host reboot.
- •In degrees mode, the SO follower default, the host does not clamp goal positions at all. Only those EEPROM limits stop an out-of-range target.
- •LeRobot caps torque and current on the gripper only. The five body joints run at the factory limits.
- •The follower's STS3215 is the 7.4 V variant: 19.5 kg.cm stall torque against 5 kg.cm rated. Feetech also sells a 12 V, 30 kg.cm STS3215, so check which one is on your arm. Working continuously above rated is how servos die.
- •There is no software emergency stop, and the arm has no brakes, so cutting power drops it. Both facts belong in the plan before the first run.
What an autonomous arm can actually do
The SO-100 is small. Small is not harmless. At 7.4 V a Feetech STS3215 gives 19.5 kg.cm of stall torque, about 1.91 Nm at the output shaft, and swings at 52 RPM unloaded, which is 312 deg/s. It will not break your arm. It will crush a fingertip against a table, snap a printed bracket, tear a USB connector out of a camera, and destroy the servo that was doing the pushing.
| Item | At 6 V | At 7.4 V |
|---|---|---|
| No-load speed | 0.238 s per 60 deg (42 RPM, 252 deg/s) | 0.192 s per 60 deg (52 RPM, 312 deg/s) |
| Stall torque | 16.5 kg.cm (1.62 Nm) | 19.5 kg.cm (1.91 Nm) |
| Rated torque | 4 kg.cm (0.39 Nm) | 5 kg.cm (0.49 Nm) |
| Stall current | 2.0 A | 2.5 A |
| Rated current | 500 mA | 650 mA |
| No-load running current | 130 mA | 150 mA |
Mind the gap between rated and stall torque: a factor of about four. A policy that parks the gripper against a fixed object asks one servo to hold near stall indefinitely, at four times its specified current. That is the most common way a first autonomous run ends, and it is silent until the plastic smells warm.
The datasheet gives a working input range of 4 V to 7.4 V and says the part enters protection outside it. The SO-100 and SO-101 use these servos; Koch v1.1 uses Dynamixel parts on 5 V and 12 V rails instead. The name alone does not tell you the voltage: the SO-ARM100 repository specifies the 7.4 V servo throughout its bill of materials, but also documents a 12 V, 30 kg.cm version of the follower motor that needs a 12 V 5 A supply instead, so confirm which variant is in your kit. A 12 V supply destroys a 7.4 V STS3215, and the servo's own overvoltage trip will not save you: the community register reference lists a factory Max_Voltage_Limit default of 140, decoding to 14.0 V, while the 7.4 V datasheet promises protection above 7.4 V. They disagree, which is why the power supply is a build decision, not a runtime one.
Layer 1: joint limits that live in the servo
The strongest limit on the arm is the one people assume is missing. When you run calibration and sweep each joint by hand, LeRobot does not only write a JSON file. It writes two registers per servo:
def write_calibration(self, calibration_dict: dict[str, MotorCalibration], cache: bool = True) -> None:
for motor, calibration in calibration_dict.items():
if self.protocol_version == 0:
self.write("Homing_Offset", motor, calibration.homing_offset)
self.write("Min_Position_Limit", motor, calibration.range_min)
self.write("Max_Position_Limit", motor, calibration.range_max)The 12-bit magnetic encoder gives counts of 0 to 4095 over a full turn at 0.088 deg per count, and once written they sit in non-volatile memory. Reboot the host, swap the laptop, restart the process: the limits stay. That is why a joint that stops early is far more often a calibration problem than a policy problem, and why no code change fixes it.
lerobot-calibrate \
--robot.type=so100_follower \
--robot.port=/dev/tty.usbmodem58760431541 \
--robot.id=my_follower_armFor reference, the SO-ARM100 project declares this in so101_new_calib.urdf, the file LeRobot's tooling recommends. These are modelling limits for kinematics, not measured servo limits, but they give the shape of the envelope you calibrate inside.
| Joint | Lower | Upper | Span |
|---|---|---|---|
| shoulder_pan | -1.91986 rad (-110.0 deg) | 1.91986 rad (110.0 deg) | 220 deg |
| shoulder_lift | -1.74533 rad (-100.0 deg) | 1.74533 rad (100.0 deg) | 200 deg |
| elbow_flex | -1.69000 rad (-96.8 deg) | 1.69000 rad (96.8 deg) | 193.7 deg |
| wrist_flex | -1.65806 rad (-95.0 deg) | 1.65806 rad (95.0 deg) | 190 deg |
| wrist_roll | -2.74385 rad (-157.2 deg) | 2.84121 rad (162.8 deg) | 320 deg |
| gripper | -0.17453 rad (-10.0 deg) | 1.74533 rad (100.0 deg) | 110 deg |
LeRobot's _unnormalize in motors_bus.py clamps in two of its three normalisation modes: RANGE_M100_100 bounds to [-100, 100], RANGE_0_100 bounds to [0, 100]. The DEGREES branch does no bounding, it computes int(val * max_res / 360 + mid) and hands the result to the bus. The SO follower ships with use_degrees = True, so the five body joints take the unclamped branch and only the gripper is clamped on the host. A policy that predicts 240 degrees on shoulder_lift gets that number encoded and sent. What stops it is the EEPROM limit from calibration, one layer down. Fine, as long as you know which layer is doing the work.
Finding the bounds you actually want
Mechanical limits are not the limits you want during an autonomous run. If the task lives in a 20 cm square in front of the arm, shoulder_pan has no business holding 220 degrees of authority. LeRobot ships a tool that measures the envelope you actually use, by teleoperating through it:
- 1Point the tool at a URDF
The kinematics need a model. The script's docstring recommends
so101_new_calib.urdffrom the SO-ARM100 repository, the file the table above came from.bashgit clone https://github.com/TheRobotStudio/SO-ARM100.git ls SO-ARM100/Simulation/SO101/so101_new_calib.urdf - 2Sweep the workspace by hand
A 5 second warmup records nothing, then 30 seconds of measurement. Drive to every corner of the region the task needs, using the leader arm or a gamepad.
bashlerobot-find-joint-limits \ --robot.type=so100_follower \ --robot.port=/dev/tty.usbmodem58760432981 \ --robot.id=my_follower_arm \ --teleop.type=so100_leader \ --teleop.port=/dev/tty.usbmodem58760434471 \ --teleop.id=my_leader_arm \ --urdf_path=SO-ARM100/Simulation/SO101/so101_new_calib.urdf \ --target_frame_name=gripper \ --warmup_time_s=5 \ --teleop_time_s=30 \ --control_loop_fps=30 - 3Keep both blocks it prints
It prints an end-effector box in metres plus a per-joint min and max. The box is the useful one, because a human can check it against the table.
text# End Effector Bounds (x, y, z): max_ee = [0.3312, 0.1904, 0.2871] min_ee = [0.0421, -0.2013, 0.0184] # Joint Position Limits (radians): max_pos = [...] min_pos = [...]
The script prints # Joint Position Limits (radians):, but takes those values straight from robot.get_observation(), and the SO follower reports degrees by default (use_degrees = True). If your maxima look like 87 rather than 1.5, the header is stale, not your arm. The end-effector bounds really are metres, since those come from the URDF.

Layer 2: torque and current caps
LeRobot does write protection registers, but far less broadly than people assume. This is the whole of it, from SOFollower.configure():
def configure(self) -> None:
with self.bus.torque_disabled():
self.bus.configure_motors()
for motor in self.bus.motors:
self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value)
self.bus.write("P_Coefficient", motor, self.config.position_p_coefficient) # 16
self.bus.write("I_Coefficient", motor, self.config.position_i_coefficient) # 0
self.bus.write("D_Coefficient", motor, self.config.position_d_coefficient) # 32
if motor == "gripper":
self.bus.write("Max_Torque_Limit", motor, 500) # 50% of max torque to avoid burnout
self.bus.write("Protection_Current", motor, 250) # 50% of max current to avoid burnout
self.bus.write("Overload_Torque", motor, 25) # 25% torque when overloadedThe asymmetry is reasonable: the gripper spends its life clamped against something, so the gripper is what burns out. But the five joints that carry the arm's mass, and that can reach the table, run at factory protection settings during an autonomous run.
| Register | Address | Factory default | What LeRobot writes | Effect |
|---|---|---|---|---|
| Max_Torque_Limit | 16, EEPROM, 2 bytes | 1000 (100%) | 500, gripper only | caps torque output at 50 percent |
| Torque_Limit | 48, RAM, 2 bytes | - | not written | runtime torque ceiling, resets on power cycle |
| Protection_Current | 28, EEPROM, 2 bytes | 0 | 250, gripper only | overcurrent threshold, about 1.6 A at 6.5 mA per count |
| Overload_Torque | 36, EEPROM, 1 byte | 80 (%) | 25, gripper only | load level that counts as a stall |
| Protective_Torque | 34, EEPROM, 1 byte | 20 (%) | not written | torque held while in overload protection |
| Protection_Time | 35, EEPROM, 1 byte | 200 | not written | how long a stall is tolerated, 2 s per the datasheet |
| Max_Temperature_Limit | 13, EEPROM, 1 byte | 70 C | not written | torque output is switched off above this |
The datasheet is precise about factory behaviour. A joint blocked above 80 percent of locked rotor for 2 seconds enters overload protection; running current above 2 A for 2 seconds enters overcurrent protection; above 70 C the servo switches torque output off. Those work. One clause changes what they mean under an autonomous policy.
The datasheet states that re-issuing the position command clears the overload protection flag, and the same for overcurrent. A LeRobot control loop writes Goal_Position on every tick. At 30 Hz a stalled joint is released from protection thirty times a second, forever. The trip assumes a human notices and stops commanding; it does not assume a loop that never stops. If you run unattended, poll Present_Temperature (address 63) and Present_Current (address 69) yourself and stop on your own threshold. See arm twitches then sags and servo not responding for the aftermath.
- A capped joint fails soft. It stalls and gives up instead of levering against a bracket until something yields.
- It protects the cheap part that is annoying to re-calibrate: the servo and its printed horn.
- Max_Torque_Limit costs nothing at runtime, one EEPROM write at connect time and no extra bus traffic per tick.
- It makes an out-of-distribution policy visibly weak rather than invisibly destructive, which is far easier to debug.
- You change the arm relative to the data you recorded. A gripper capped at 50 percent may no longer hold the object the demonstrations show it holding, so the policy looks worse for reasons unrelated to training.
- Tasks that need force (pressing a button, opening a drawer, seating a connector) start failing, and you chase it in the model instead of the config.
- EEPROM writes need torque disabled first, so you cannot tune this mid-run.
- A cap protects the servo, not the workpiece. The arm can still be stopped by your fingers and still be pressing on them.
Layer 3: the per-step cap almost nobody turns on
The default here will surprise you. SOFollowerConfig.max_relative_target is typed float | dict[str, float] | None and defaults to None. With no cap set, send_action skips the clamp and writes whatever the policy produced. A LeRobot issue asking what a sensible value would be was closed as stale with no maintainer answer, which tells you how much settled guidance exists.
def ensure_safe_goal_position(
goal_present_pos: dict[str, tuple[float, float]], max_relative_target: float | dict[str, float]
) -> dict[str, float]:
"""Caps relative action target magnitude for safety."""
...
for key, (goal_pos, present_pos) in goal_present_pos.items():
diff = goal_pos - present_pos
max_diff = diff_cap[key]
safe_diff = min(diff, max_diff)
safe_diff = max(safe_diff, -max_diff)
safe_goal_pos = present_pos + safe_diff
safe_goal_positions[key] = safe_goal_posTurning it on is one flag, taking a scalar for every joint or a dict so you can be strict where gravity is:
# one cap for every joint
lerobot-rollout \
--strategy.type=base \
--policy.path=${HF_USER}/my_policy \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM1 \
--robot.max_relative_target=5 \
--task="put the brick in the box" \
--duration=60
# per joint, in degrees, tighter on the joints that carry the arm
--robot.max_relative_target='{shoulder_pan: 4, shoulder_lift: 3, elbow_flex: 4, wrist_flex: 6, wrist_roll: 10, gripper: 20}'Here is what catches people: the cap is per control tick, not a velocity limit, so the same number produces very different arms depending on loop rate. With inference latency of 20 ms for ACT and 485 ms for Pi0.5, at one policy call per tick, a five degree cap means this:
| Model | Inference per action step | 5 deg per tick becomes | What that feels like |
|---|---|---|---|
| ACT | 20 ms | about 250 deg/s | barely below the servo's 312 deg/s no-load ceiling, so the cap hardly bites |
| GR00T N1.7 | 152 ms | about 33 deg/s | deliberate, visibly slower than teleop |
| GR00T N1.5 | 165 ms | about 30 deg/s | same neighbourhood as N1.7 |
| SmolVLA | 245 ms | about 20 deg/s | slow enough to walk over and intervene |
| Pi0.5 | 485 ms | about 10 deg/s | crawling; a bad target takes seconds to become a collision |
| Any model, chunk replayed at 30 Hz | 33 ms per tick | 150 deg/s | what you actually get with action chunking, not the per-call figure |
The last row matters most. With action chunking the model is not called every tick: it emits a chunk and the loop replays it at a fixed rate, which is what LeRobot's --inference.type=rtc path is for on the slow VLAs. Under chunk replay the cap applies at the replay rate, so 5 degrees at 30 Hz is 150 deg/s whichever model produced the chunk. Pick the cap against your loop rate, then watch the arm at that speed before you trust it.
With max_relative_target set, send_action must know where the arm is, so it runs a sync_read("Present_Position") before every write. LeRobot's own comment on that line reads /!\ Slower fps expected due to reading from the follower. On a bus already spending most of its tick budget on camera reads that is a real cost, and it shows up as dropped ticks. lerobot-record, lerobot-teleoperate and lerobot-rollout all print a cadence summary with the share of ticks that blew the budget, so measure before and after.
Layer 4: workspace bounds, which nothing enforces
A joint limit is not a box in space. Every joint can sit inside its range while the end effector is 5 cm below the table. On a 5 degree-of-freedom arm you cannot eyeball the map from joint space to Cartesian space, which is why the tool prints the end-effector box separately.
The honest state of it: LeRobot gives you the kinematics and the measurement tool, and no runtime filter. There is no max_ee config field. If you want a Cartesian bound respected during a rollout, you write it:
import numpy as np
from lerobot.model import RobotKinematics
kin = RobotKinematics("so101_new_calib.urdf", "gripper")
LO = np.array([0.02, -0.22, 0.015]) # metres, from lerobot-find-joint-limits
HI = np.array([0.34, 0.20, 0.30])
def guarded_send(robot, action):
q = np.array([action[f"{m}.pos"] for m in robot.bus.motors])
ee = kin.forward_kinematics(q)[:3, 3]
if np.any(ee < LO) or np.any(ee > HI):
obs = robot.get_observation()
hold = {f"{m}.pos": obs[f"{m}.pos"] for m in robot.bus.motors}
return robot.send_action(hold) # freeze in place
return robot.send_action(action)Doing this properly is an active research area, not a config flag. Any-Body Guard (Beaudin et al., June 2026) builds the filter in configuration space from forward kinematics and object-centric scene representations, reports zero collisions in its hardware experiments, and transfers across embodiments without retraining. That is the direction of travel, and it is still research code on research setups rather than something you drop into a LeRobot config this afternoon.
The stop button
Ask what happens when you need the arm stopped right now. The keyboard controls in lerobot-record (Right or n, Left or r, Esc or q) are recording flow control, not a stop. The real one is lerobot-rollout --interactive=true: the robot stays idle until you type /start, and /stop ends the control loop from stdin. Ctrl-C works too, but it tears down through disconnect(), where disable_torque_on_disconnect defaults to True, so the arm goes limp. On a raised arm, limp means it falls.
| Stop | How fast | What the arm does | Fails when |
|---|---|---|---|
| Type /stop in an interactive rollout | one tick plus your reaction time | no new targets, torque stays on, arm holds its pose | the loop is blocked, the terminal is not focused, or the host is somewhere else |
| Ctrl-C in the rollout terminal | one tick plus teardown | torque disabled on disconnect, arm goes limp and drops | a hung serial read delays teardown by seconds |
| Unplug the USB cable | immediate | servos hold the last Goal_Position with torque on | the arm is already pressing into something; it keeps pressing |
| Kill power at an inline switch | immediate | torque off, arm falls | anything fragile is under the arm |
| Hold the arm with your hand | immediate | about 1.9 Nm per joint, you win | it is already on your finger |
IEC 60204-1 clause 9.2.2 defines stop categories: category 0 is stopping by immediate removal of power, category 1 is a controlled stop with power available and then removed, category 2 is a controlled stop with power left on. Only 0 and 1 are permitted for an emergency stop function. Every option in the table above is a hobby approximation of 0 or 2, implemented in the same software stack that might be the thing failing, with no redundancy and no diagnostic coverage. Industrial arms get this from ISO 10218-1 and ISO 13850 conformant hardware channels. Your SO-100 has none of it. Say "power switch", not "emergency stop", and design the bench so the worst case is survivable rather than merely unlikely.
- 1Put a switch on the DC side
An inline rocker or latching mushroom button between the power supply and the motor control board. Reachable with one hand while the other is on the keyboard, and not behind the arm.
textPSU --> [ switch ] --> motor control board --> servo bus USB stays connected - 2Clamp the arm down
The SO-ARM100 bill of materials includes table clamps for both arms. This is not tidiness: an unclamped arm that pushes against a fixed object levers itself off the desk instead of stalling.
- 3Clear the drop zone
There are no brakes, so cutting power drops the arm through whatever is under it. Nothing you care about goes under a raised arm, cameras included.
- 4Start idle, then say go
Interactive mode runs connect, calibrate and camera-open while the arm is stationary. Motion starts only when you type
/start, with your hand near the switch.bashlerobot-rollout \ --strategy.type=base \ --policy.path=${HF_USER}/my_policy \ --robot.type=so100_follower \ --robot.port=/dev/ttyACM1 \ --robot.max_relative_target=5 \ --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \ --task="put the brick in the box" \ --interactive=true

Two ways to get to the same guarded run
Everything above, on your own bench. You own the whole chain: serial ports, calibration files, cap values, and the switch.
pip install lerobot
# 1. sweep the real limits into servo EEPROM
lerobot-calibrate --robot.type=so100_follower --robot.port=/dev/ttyACM1 --robot.id=arm1
# 2. measure the envelope the task needs
lerobot-find-joint-limits --robot.type=so100_follower --robot.port=/dev/ttyACM1 \
--robot.id=arm1 --teleop.type=so100_leader --teleop.port=/dev/ttyACM0 --teleop.id=lead1 \
--urdf_path=SO-ARM100/Simulation/SO101/so101_new_calib.urdf --teleop_time_s=30
# 3. run guarded, idle until you say go
lerobot-rollout --strategy.type=base --policy.path=$HF_USER/my_policy \
--robot.type=so100_follower --robot.port=/dev/ttyACM1 --robot.id=arm1 \
--robot.max_relative_target=5 --interactive=true --task="pick up the cube" --duration=60- You set max_relative_target explicitly, because the default is None.
- You decide whether Max_Torque_Limit goes on the body joints too, not just the gripper.
- You poll Present_Temperature yourself if the run is unattended.
- You buy and wire the switch. Nobody ships one with the arm.
The platform handles the parts either side of the safety layer: recording, training on a rented GPU, and serving the checkpoint back to your robot client. It does not handle the safety layer itself.
- The desktop client records LeRobot-format datasets from a teleop session, so the arm you calibrate is the arm the data describes.
- Training rents a GPU by required VRAM and writes checkpoints to object storage: roughly 4 to 12 USD on the A100 or H100 tier, roughly 1 to 3 USD on the 4090 tier.
- Inference pods auto-provision and carry an idle watchdog that destroys them after an idle period, so a forgotten pod stops billing. That is a budget watchdog, not a robot watchdog.
- The failure-mode pages cover what a missing safety layer produces, from a joint stopping early to a policy freezing mid-motion.
- Live puts a real arm in your browser with no signup, queue-based, so you can watch a policy move hardware you are not responsible for.
There is no remote stop worth trusting. The control loop is 20 to 485 ms per action step depending on the model, and public-internet round trips on top of that turn a working policy into a hesitant one. A stop command takes the same path as the actions, so it arrives late for the same reason. Remote inference is viable for slow pick-and-place, not for fast reactive motion; remote stopping is not viable at all. The switch goes on your bench, next to the servos.
Where all of this still does not save you
Four gaps are structural, and being straight about them beats a longer checklist.
- A per-step cap does not bound the destination. Clamping each tick to 5 degrees stops the jerk, not the journey. A policy convinced the target is inside the table walks there in 5 degree increments and arrives with full torque. The cap buys reaction time, which only helps if someone is reacting.
- Torque caps change the distribution. The demonstrations were recorded on an uncapped arm. Cap it and the policy is driving hardware it never saw. See policy only works in one setup.
- Servo protections do not latch under a control loop. Overload and overcurrent flags clear when a new position command arrives, and your loop sends one every tick. Temperature is the honest signal.
- Nothing here is redundant. Every layer runs in one process on one USB bus with no diagnostic coverage. If the serial link hangs, the servos hold their last target and none of your software runs at all. That is what the switch is for.

Pre-flight checklist for the first autonomous run
- Calibration is fresh and swept to the real mechanical stops, so the EEPROM position limits mean something.
- max_relative_target is a number, not None, and you have worked out what it becomes in deg/s at your loop rate.
- The arm and its base are clamped to the table.
- The power switch is on the DC side, reachable with your free hand, and not behind the arm.
- Nothing you care about is inside the arm's reach or under it, cameras included.
- The run starts with --interactive=true, so the arm is idle until you type /start.
- You know your Present_Temperature threshold and how you would notice it crossing.
- The first run is short. Use --duration=30 and take the win of a boring 30 seconds.
None of this makes a learned policy safe. It makes its mistakes cheap, which is the achievable goal on a desk arm. If the rest of the pipeline is still going up, the SO-100 setup and training guide covers parts to first episode, and the data-quality guide covers why recording conditions decide how confused the policy will be when it finally drives the arm alone.
When the arm does something you did not expect
A joint that stops early, an arm that twitches then sags, a policy that freezes mid-motion, a gripper that will not close. Each page names the check that tells you which layer failed.
Open the failure-mode indexWhat is a sensible value for max_relative_target on an SO-100?▾
There is no upstream recommendation; the LeRobot issue asking exactly this was closed as stale without an answer. Reason about it rather than copying a number: the cap is per control tick, so multiply by your loop rate to get deg/s and compare with the STS3215's 312 deg/s no-load speed at 7.4 V. Five degrees at 30 Hz is 150 deg/s, about half the servo ceiling, a defensible first run. Tighten shoulder_lift and elbow_flex further, since those carry the arm's mass.
Does LeRobot limit torque on the SO-100 by default?▾
Only on the gripper. SOFollower.configure() writes Max_Torque_Limit 500, Protection_Current 250 and Overload_Torque 25 inside an if that matches the gripper motor. The five body joints get PID gains (P 16, I 0, D 32) and nothing else, so they run at factory protection: overload above 80 percent of locked rotor for 2 s, overcurrent above 2 A for 2 s, torque off above 70 C.
If the servo has overload protection, why do servos still burn out?▾
Because the protection flag is cleared by the next position command, per the Feetech datasheet, and a control loop sends one every tick. At 30 Hz it is cleared thirty times a second. The trip assumes a human notices the servo has given up and stops commanding it. Under an autonomous policy, poll Present_Temperature at address 63 and stop on your own threshold.
Can I stop the arm from the browser or from the cloud?▾
Not in any way you should rely on. The control loop is 20 to 485 ms per action step depending on the model, and a stop sent over the public internet takes the same path as the actions, so it arrives with the same delay. Remote inference is workable for slow pick-and-place; remote stopping is not. The switch belongs on the bench.
Do the joint limits survive if I reinstall lerobot or change laptops?▾
Yes. Calibration writes range_min and range_max into the servo's Min_Position_Limit (address 9) and Max_Position_Limit (address 11) EEPROM registers, plus Homing_Offset. They are non-volatile, which is also why a bad calibration gives you an arm that stops early no matter what you change in software. Re-run lerobot-calibrate to fix it.
Is a 5 DOF arm safer than a 6 DOF one?▾
Not for these purposes. Fewer joints means a smaller and less dexterous reachable set, but on an SO-100 follower every joint is the same 1/345 STS3215 delivering about 1.9 Nm, and none of them has a brake. What changes between arms is the mass swung and the voltage: the SO-100 and SO-101 bill of materials specifies the 7.4 V STS3215, and a 12 V, 30 kg.cm version of the same servo exists; a Koch v1.1 uses Dynamixel servos on 5 V and 12 V rails; and a LeKiwi adds a 12 V mobile base that can drive the whole arm off a table.
Sources
- LeRobot: SOFollowerConfig, max_relative_target and disable_torque_on_disconnect defaults
- LeRobot: SOFollower.configure() and send_action, the gripper-only torque caps
- LeRobot: ensure_safe_goal_position, the per-step clamp
- LeRobot: STS/SMS control table with protection register addresses
- LeRobot: lerobot-find-joint-limits, joint and end-effector bounds by teleoperation
- LeRobot: lerobot-rollout, interactive mode with /start and /stop
- LeRobot issue 1483: how should max_relative_target be set (closed, unanswered)
- LeRobot docs: imitation learning on real robots, recording controls and rollout strategies
- Feetech STS3215 product specification A/0, 2020-04-10 (translated)
- Community STS3215 register reference: addresses, defaults and scaling
- SO-ARM100: bill of materials, servo variants and power supply notes
- SO-ARM100: so101_new_calib.urdf with per-joint limits
- Any-Body Guard: Universal Safeguarding for Manipulation Policies via Action Masking (2026)
- IEC 60204-1:2016+AMD1:2021, Safety of machinery: electrical equipment of machines
- IEC 60204-1:2016 preview: clause 9.2.2 categories of stop functions, 10.7 emergency stop devices
Sources
- LeRobot: SOFollowerConfig, max_relative_target and disable_torque_on_disconnect defaults
- LeRobot: SOFollower.configure() and send_action, the gripper-only torque caps
- LeRobot: ensure_safe_goal_position, the per-step clamp
- LeRobot: STS/SMS control table with protection register addresses
- LeRobot: lerobot-find-joint-limits, joint and end-effector bounds by teleoperation
- LeRobot: lerobot-rollout, interactive mode with /start and /stop
- LeRobot issue 1483: how should max_relative_target be set (closed, unanswered)
- LeRobot docs: imitation learning on real robots, recording controls and rollout strategies
- Feetech STS3215 product specification A/0, 2020-04-10 (translated)
- Community STS3215 register reference: addresses, defaults and scaling
- SO-ARM100: bill of materials, servo variants and power supply notes
- SO-ARM100: so101_new_calib.urdf with per-joint limits
- Any-Body Guard: Universal Safeguarding for Manipulation Policies via Action Masking (2026)
- IEC 60204-1:2016+AMD1:2021, Safety of machinery: electrical equipment of machines
- IEC 60204-1:2016 preview: clause 9.2.2 categories of stop functions and 10.7 emergency stop devices
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started