The AY-Robots try page showing three ways to start without owning a robot: drive a real arm, compare models, rent a GPU
Robot safetySO-100LeRobotFeetech STS3215Policy deployment

Safety Limits for an Autonomous Arm: Joints, Torque, Stop

AY-Robots ResearchAugust 23, 202621 min read

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.

ItemAt 6 VAt 7.4 V
No-load speed0.238 s per 60 deg (42 RPM, 252 deg/s)0.192 s per 60 deg (52 RPM, 312 deg/s)
Stall torque16.5 kg.cm (1.62 Nm)19.5 kg.cm (1.91 Nm)
Rated torque4 kg.cm (0.39 Nm)5 kg.cm (0.49 Nm)
Stall current2.0 A2.5 A
Rated current500 mA650 mA
No-load running current130 mA150 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.

7.4 V means 7.4 V

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:

python
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)
src/lerobot/motors/feetech/feetech.py. Min_Position_Limit is EEPROM address 9, Max_Position_Limit is address 11, both two bytes, both in raw encoder counts.

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.

bash
lerobot-calibrate \
  --robot.type=so100_follower \
  --robot.port=/dev/tty.usbmodem58760431541 \
  --robot.id=my_follower_arm
Sweep every joint to its mechanical stop and back. What you sweep is what the servo will refuse to leave.

For 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.

JointLowerUpperSpan
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
The host does not clamp in degrees mode

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:

  1. 1
    Point the tool at a URDF

    The kinematics need a model. The script's docstring recommends so101_new_calib.urdf from the SO-ARM100 repository, the file the table above came from.

    bash
    git clone https://github.com/TheRobotStudio/SO-ARM100.git
    ls SO-ARM100/Simulation/SO101/so101_new_calib.urdf
  2. 2
    Sweep 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.

    bash
    lerobot-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
  3. 3
    Keep 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 = [...]
Read the numbers, not the header

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.

The AY-Robots CLI page showing the install command and the run commands for driving an arm and starting a policy from a terminal
The same operations from a terminal. Whichever entry point you use, the safety flags are per-robot config and live on the machine that owns the serial port.

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():

python
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 overloaded
src/lerobot/robots/so_follower/so_follower.py. Note the indentation: the three protection writes are inside an if for the gripper. shoulder_pan, shoulder_lift, elbow_flex, wrist_flex and wrist_roll get PID gains and nothing else.

The 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.

RegisterAddressFactory defaultWhat LeRobot writesEffect
Max_Torque_Limit16, EEPROM, 2 bytes1000 (100%)500, gripper onlycaps torque output at 50 percent
Torque_Limit48, RAM, 2 bytes-not writtenruntime torque ceiling, resets on power cycle
Protection_Current28, EEPROM, 2 bytes0250, gripper onlyovercurrent threshold, about 1.6 A at 6.5 mA per count
Overload_Torque36, EEPROM, 1 byte80 (%)25, gripper onlyload level that counts as a stall
Protective_Torque34, EEPROM, 1 byte20 (%)not writtentorque held while in overload protection
Protection_Time35, EEPROM, 1 byte200not writtenhow long a stall is tolerated, 2 s per the datasheet
Max_Temperature_Limit13, EEPROM, 1 byte70 Cnot writtentorque 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 protection flag is cleared by the next position command

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.

Capping torque before an autonomous run
Advantages
  • 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.
Trade-offs
  • 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.

python
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_pos
src/lerobot/robots/utils.py. A symmetric per-joint clamp on the delta from the measured present position, with a warning logged whenever it bites.

Turning it on is one flag, taking a scalar for every joint or a dict so you can be strict where gravity is:

bash
# 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}'
A dict must name every motor. ensure_safe_goal_position raises ValueError if the key sets do not match exactly.

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:

ModelInference per action step5 deg per tick becomesWhat that feels like
ACT20 msabout 250 deg/sbarely below the servo's 312 deg/s no-load ceiling, so the cap hardly bites
GR00T N1.7152 msabout 33 deg/sdeliberate, visibly slower than teleop
GR00T N1.5165 msabout 30 deg/ssame neighbourhood as N1.7
SmolVLA245 msabout 20 deg/sslow enough to walk over and intervene
Pi0.5485 msabout 10 deg/scrawling; a bad target takes seconds to become a collision
Any model, chunk replayed at 30 Hz33 ms per tick150 deg/swhat 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.

The cap is not free

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:

python
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)
A sketch, not library code. Note the failure mode it introduces: a frozen policy keeps observing the same scene and keeps predicting the same rejected action, so a freeze is a stop, not a recovery. Log it and end the episode.

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.

StopHow fastWhat the arm doesFails when
Type /stop in an interactive rolloutone tick plus your reaction timeno new targets, torque stays on, arm holds its posethe loop is blocked, the terminal is not focused, or the host is somewhere else
Ctrl-C in the rollout terminalone tick plus teardowntorque disabled on disconnect, arm goes limp and dropsa hung serial read delays teardown by seconds
Unplug the USB cableimmediateservos hold the last Goal_Position with torque onthe arm is already pressing into something; it keeps pressing
Kill power at an inline switchimmediatetorque off, arm fallsanything fragile is under the arm
Hold the arm with your handimmediateabout 1.9 Nm per joint, you winit is already on your finger
None of this is a safety-rated stop

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.

  1. 1
    Put 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.

    text
    PSU --> [ switch ] --> motor control board --> servo bus
                            USB stays connected
  2. 2
    Clamp 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.

  3. 3
    Clear 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.

  4. 4
    Start 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.

    bash
    lerobot-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
The AY-Robots failure-mode index listing problems such as a joint stopping early, an arm twitching then sagging, and a policy freezing mid-motion
Most entries on the failure-mode index are symptoms of a missing safety layer, not a bad model. Worth reading before the first autonomous run rather than after.

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.

bash
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.

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.
The AY-Robots MCP server page listing the platform operations exposed to AI agents as tools
The MCP server exposes the same operations to an agent that the CLI exposes to you. Worth saying plainly: an agent can start a training run, and it cannot reach your power switch.

Pre-flight checklist for the first autonomous run

  1. Calibration is fresh and swept to the real mechanical stops, so the EEPROM position limits mean something.
  2. max_relative_target is a number, not None, and you have worked out what it becomes in deg/s at your loop rate.
  3. The arm and its base are clamped to the table.
  4. The power switch is on the DC side, reachable with your free hand, and not behind the arm.
  5. Nothing you care about is inside the arm's reach or under it, cameras included.
  6. The run starts with --interactive=true, so the arm is idle until you type /start.
  7. You know your Present_Temperature threshold and how you would notice it crossing.
  8. 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 index
What 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.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started