
The Feetech STS3215 is tested to 100,000 cycles at one fifth of stall torque. What that means for a recording rig, which joint dies first, and the registers that warn you.
An SO-100 rarely dies in one dramatic moment. What happens is slower and more irritating. The gripper stops closing the last two millimetres. The shoulder sags a degree when you park it. A joint that used to hold still now hums when the arm is extended. None of that stops a recording session, which is exactly the problem: the rig keeps working well enough to produce episodes that are subtly worse than the ones you recorded last month, and three weeks later you are debugging a policy that was never the thing at fault.
This page covers what wears out first on a Feetech STS3215 arm, what the vendor's numbers actually promise, which registers warn you before a joint gives up, and a maintenance routine short enough that you will really run it. Hardware numbers come from three documents opened for this article: the Feetech STS3215 product specification (edition A/1, 2023-06-23, the 7.4 V 19 kg.cm part the SO-100 bill of materials calls C001), the ST3215 memory table V3.7, and the lerobot source at tag v0.6.1, released 2026-08-03. Those three disagree in four places, each named where it comes up rather than smoothed over.
What you need to know
- •Feetech's only published wear figure is a life test of more than 100,000 cycles at one fifth of stall torque on a 1.5 second cycle: about 42 hours of continuous motion under a light bench load, not a lifetime rating for an arm holding itself up.
- •The gripper fails first, because stalling it against an object is the normal way to use it. A lerobot maintainer's diagnosis on issue 2819 is exactly that: overloaded too long.
- •Four values see wear coming: Present_Temperature (address 63), Present_Load (60), Present_Current (69) and the servo status byte (65). All are readable while the arm runs, and Present_Current converts to torque with the datasheet's Kt of 7.8 kg.cm per amp.
- •The vendor documents disagree on the over-current trip point, the return delay default and the status bit names, and an independent bench run saw no thermal cut-off at all. Trends beat thresholds.
- •In recorded data, wear appears as a widening gap between action and observation.state on one joint at one pose. Compare the same pose across weeks, never one joint against another.
- •Swapping a servo means a full recalibration, so every dataset recorded before the swap sits on a slightly different zero.
What the datasheet actually promises
The SO-100 and the SO-101 both run six Feetech STS3215 bus servos at 7.4 V, and the follower uses the C001 variant with 1/345 gearing on all six joints. That part number matters, because most of the bench reports you will find online were run on the 12 V variant, which is a different motor with a different torque curve. Here is what the 7.4 V specification says about the servo you actually have.
| Specification | STS3215 at 7.4 V | Note |
|---|---|---|
| Stall torque | 19.5 kg.cm | 16.5 kg.cm at 6 V |
| Rated torque | 5 kg.cm | 4 kg.cm at 6 V; the load it is built to carry continuously |
| Stall current | 2.5 A | 2 A at 6 V |
| Rated current | 650 mA | at rated torque; 150 mA no-load, 21 mA idle |
| Torque constant Kt | 7.8 kg.cm per amp | 2.5 A x 7.8 = 19.5 kg.cm, consistent with stall torque |
| Rated input voltage | 4 V to 7.4 V | protection trips above 8 V or below 4 V, and clears itself |
| Gear ratio | 1/345 | copper gears, PA+GF case, ball bearings, 55 g |
| Encoder | 12 bit magnetic, 4096 steps | 0.088 degrees per step, and the datasheet lists sensor life as no limit |
| Backlash | 0.5 degrees or less | a specification, not a measurement of your unit |
| Operating temperature | -20 C to 60 C | storage -30 C to 80 C |
| Noise | 60 +/- 5 dB at 30 cm | the baseline a gearbox gets louder than |
| Life test | more than 100,000 cycles | 60 deg out in 0.25 s, pause 0.5 s, 60 deg back, pause 0.5 s, at one fifth of stall torque; the only wear figure Feetech publishes |
One item on the list does not wear at all. The angle sensor is a 12 bit magnetic type whose life the datasheet gives as no limit, so unlike a potentiometer servo the position feedback does not degrade with the rest of the unit. That cuts both ways. Position readings stay trustworthy, which is what makes the register checks below worth doing. But everything mechanical between the sensor and the workpiece does wear: gear backlash, the fit of the horn on the printed part, screws seated in plastic. A clean-looking observation.state is not evidence of a healthy arm, only of an honest encoder.
The life test is the only number Feetech publishes about wear, and it is worth doing the arithmetic. One cycle takes 1.5 seconds, so 100,000 cycles is 150,000 seconds, about 42 hours of continuous motion. The load is one fifth of stall torque, which at 7.4 V is 3.9 kg.cm, comfortably under the 5 kg.cm rated torque. So the headline figure describes a light, fixed duty cycle on a bench, not a shoulder joint holding a forearm out while a policy drives it at full commanded acceleration. Treat it as an order of magnitude: tens of hours of motion under a light load, not thousands. No per-joint torque profile for the SO-100 reference build has been published that survives checking, so do not convert this into a service interval and believe it.
What breaks first, and in what order
Failures on this arm are not random across the six joints. Load, duty and the way people use the arm all push in the same direction, and the pattern is consistent enough to plan spares around. Bus ids below are the ones lerobot assigns in the SO follower definition, gripper last at id 6.
| Joint (bus id) | Why it wears | First symptom | Where it shows up |
|---|---|---|---|
| gripper (6) | Stalling against an object is the intended use, and the leader keeps commanding closure after contact | Grip force falls; objects slip late in an episode, not at pickup | Present_Load pinned high while position barely changes |
| shoulder_lift (2) | Holds the arm against gravity all session, including while parked | Sag at the rest pose, warm case after a long block | Highest Present_Temperature of the six, non-zero Present_Load at standstill |
| elbow_flex (3) | Second largest gravity moment, and moves through most of its range every episode | Overshoot and hunting instead of settling | Repeated position error at the same waypoint |
| shoulder_pan (1) | Low gravity load, but it swings the whole arm's inertia | Vibration when the arm is extended | Oscillation visible in observation.state while nothing is commanded |
| wrist_flex (4), wrist_roll (5) | Light loads, but many small reversals per episode | Play you can feel by hand at the end effector | Hysteresis: different resting position depending on approach direction |
| Printed parts and screws | M2x6 motor and M3x6 horn screws pulling into plastic over many load cycles | Play that feels exactly like servo backlash but is not | Disappears when you tighten and returns within a week |
The gripper case is the one most people hit. On lerobot issue 2819 a user reported a follower gripper servo that went stiff and then stopped working, and a maintainer named the mechanism: the servo can break when overloaded for too long, for example if you close the teleoperation gripper fully while the follower gripper is already holding something. In leader-follower teleoperation the leader has no idea the follower has hit an object. The operator squeezes the handle, the follower gripper stalls, and it keeps stalling until somebody lets go. That is why lerobot writes a torque ceiling into the follower gripper and nowhere else.
First: voltage. The STS3215 on an SO-100, an SO-101 and the arm of a LeKiwi is the 7.4 V part, rated for 4 V to 7.4 V in. A 12 V supply on that bus destroys them. A 12 V STS3215 with 30 kg.cm stall torque exists and the SO-ARM100 bill of materials links it, but it is a separate part number needing its own 12 V 5 A supply. Check the part number on every servo before the first power-up, and be especially careful on a LeKiwi, where a 12 V rail for the base and a 7.4 V rail for the arm live in the same robot. Second: parked stalls. Leaving the follower gripper clamped on an object with torque enabled, or the arm powered in a pose the shoulder has to fight, runs the overload timer for as long as you are away making coffee.
The leader arm wears differently
The follower fights gravity all session. The leader fights the operator, and that difference is built into the hardware. On the SO-101 the follower is six identical 1/345 servos, while the leader mixes three gear ratios so it can hold its own weight without being stiff in the hand. The lerobot assembly guide gives the assignment per joint, which is what you need before ordering a spare.
| Leader joint (bus id) | Gear ratio | Part number | Count in the leader |
|---|---|---|---|
| Base / shoulder pan (1) | 1/191 | C044 | 2 with elbow flex |
| Shoulder lift (2) | 1/345 | C001 | 1 |
| Elbow flex (3) | 1/191 | C044 | see above |
| Wrist flex (4) | 1/147 | C046 | 3 with wrist roll and gripper |
| Wrist roll (5) | 1/147 | C046 | see above |
| Gripper (6) | 1/147 | C046 | see above |
The SO-ARM100 README states the reason the SO-101 exists in one line: improved wiring, easier assembly with no gear removal, and updated motors for the leader arm. The gear removal it refers to belonged to the older SO-100 leader build. If you still run an SO-100 leader whose servos were modified that way, those units sit outside anything the datasheet describes, and they are the first place to look when the leader feels notchy under the hand. The follower is unaffected either way.
The registers that see it coming
The STS3215 reports more about itself than most people ever read, and all of it comes over the same serial bus that carries position commands, so you can sample it during a real session instead of setting up a test. Addresses and units below come from the ST3215 memory table V3.7; the names are the keys lerobot uses in its Feetech control table, so they copy straight into code.
| Register | Address | Unit | Factory value | What a worrying value looks like |
|---|---|---|---|---|
| Present_Temperature | 63 | degrees C | read-only | Above 50 C on one joint while the others sit near ambient |
| Present_Load | 60 | 0.1 percent, sign bit 10 | read-only | Non-zero and rising at a standstill pose that used to read near zero |
| Present_Current | 69 | 6.5 mA per count | read-only | Steady draw climbing week over week for the same motion |
| Status | 65 | bit flags | read-only | Any non-zero value; bit 5 (overload) and bit 2 (temperature) are the ones that matter here |
| Max_Temperature_Limit | 13 | degrees C | 70 | Do not raise it. If you are tempted, the joint is telling you something |
| Max_Torque_Limit | 16 | 0.1 percent of stall | 1000 (100 percent) | Copied into Torque_Limit (48) at power-up |
| Protection_Current | 28 | 6.5 mA per count | 500 (3250 mA) | The over-current trip point, but see the conflict below |
| Overload_Torque | 36 | percent of max torque | 80 | Threshold that starts the overload countdown |
| Protection_Time | 35 | 10 ms per count | 200 (2 s) | Time above the threshold before protection engages, 2.5 s maximum |
| Protective_Torque | 34 | percent of max torque | 20 | Output torque held after protection engages |
| Unloading_Condition | 19 | bit flags | 44 | Which errors actually cut torque; see the callout below |
One honest caveat about Present_Load: the memory table describes it as the voltage duty cycle of the control output driving the motor, not a calibrated torque reading. It is a proxy, and still worth logging, because a duty cycle that climbs for the same motion means the servo is working harder for the same result. If you want a number with units, use Present_Current and the Kt constant instead.
from lerobot.motors import Motor, MotorNormMode
from lerobot.motors.feetech import FeetechMotorsBus
JOINTS = ["shoulder_pan", "shoulder_lift", "elbow_flex",
"wrist_flex", "wrist_roll", "gripper"]
# Status byte at address 65. Bit names from the ST3215 memory table V3.7.
# The SCServo SDK labels bit 1 "angle" and has no constant for bit 4 at all,
# so do not take these names from the SDK.
ERR_BITS = [(1, "voltage"), (2, "sensor"), (4, "temperature"),
(8, "current"), (16, "angle"), (32, "overload")]
bus = FeetechMotorsBus(
port="/dev/ttyACM0", # from lerobot-find-port
motors={n: Motor(i + 1, "sts3215", MotorNormMode.RANGE_M100_100)
for i, n in enumerate(JOINTS)},
)
bus.connect()
print(f"{'joint':<14}{'temp C':>8}{'volt V':>8}{'load %':>8}{'mA':>8} status")
for name in bus.motors:
try:
temp = bus.read("Present_Temperature", name)
volt = bus.read("Present_Voltage", name) * 0.1
# lerobot decodes the sign bit (bit 10) of Present_Load for you
load = abs(bus.read("Present_Load", name)) / 10.0
curr = bus.read("Present_Current", name) * 6.5
stat = bus.read("Status", name)
except RuntimeError as exc:
# read() raises as soon as the status packet carries a non-zero error
# field, so a motor that is already in overload reports here, not below
print(f"{name:<14}{'read refused':>32} {exc}")
continue
flags = ",".join(n for b, n in ERR_BITS if stat & b) or "ok"
print(f"{name:<14}{temp:>8}{volt:>8.1f}{load:>8.1f}{curr:>8.0f} "
f"{flags} ~{curr / 1000 * 7.8:.1f} kg.cm")
bus.disconnect() # disable_torque=True by default
Run that twice: once cold, once right after a session. The cold reading is your baseline and it should be boring; the hot reading is the one that ages. Log both with a date, because the value of these numbers is entirely in the trend. If the script cannot reach a motor at all, that is a different problem and the servo not responding and arm not detected pages cover it.
The obvious way to write this script is to read the status byte and print the flags. It does not work that way. In lerobot v0.6.1, MotorsBus.read hands the transfer to _read with raise_on_error=True, which raises a RuntimeError as soon as the status packet's error field is non-zero. A servo sitting in overload is therefore the one servo whose Status register you cannot simply read: the read blows up first, with a message such as Overload error! from the SDK's packet decoder. Catch RuntimeError per joint, as the script above does, or your health check dies on exactly the motor you wrote it for.
The memory table gives address 65 six error flags: bit 0 voltage, bit 1 sensor, bit 2 temperature, bit 3 current, bit 4 angle, bit 5 overload. The SCServo Python SDK defines only five constants and disagrees on one: ERRBIT_VOLTAGE = 1, ERRBIT_ANGLE = 2, ERRBIT_OVERHEAT = 4, ERRBIT_OVERELE = 8, ERRBIT_OVERLOAD = 32. The SDK calls bit 1 the angle error; the vendor table calls bit 1 the sensor error and puts angle on bit 4, which the SDK does not define at all. Two registers then decide what the servo does about a flag: Unloading_Condition (19, default 44 = bits 2, 3, 5) and LED_Alarm_Condition (20, default 47 = bits 0, 1, 2, 3, 5). So out of the box only temperature, current and overload cut torque; voltage and sensor errors just flash the LED; an angle error does neither until you enable it.
Where the vendor's own documents disagree
The numbers people quote for protection thresholds come from whichever document they happened to open. Four disagreements turned up while checking this article, and none has a resolution a reader could verify, so they are listed rather than picked.
| Question | One document says | The other says | What to do |
|---|---|---|---|
| Over-current trip point | Datasheet: output shuts off after 2 s above 2 A | Memory table: Protection_Current default 500, which is 3250 mA | Read the register on your own motor rather than assuming either |
| Return_Delay_Time default | Memory table: initial value 0 | lerobot source comment: motors ship at 250, meaning 500 us | Does not matter in practice, because lerobot writes 0 on every connect |
| Status bit 1 | Memory table: sensor error, angle on bit 4 | SCServo SDK: ERRBIT_ANGLE = 2, no bit 4 constant | Use the memory table names and expect SDK messages to differ |
| Thermal cut-off at 70 C | Datasheet: torque output closes above 70 C | Bench run on the 12 V part hit 71 C with no shutdown observed, and recommends a manual cutoff | Do not rely on the servo saving itself; watch Present_Temperature |
Overload protection engages after the load stays above Overload_Torque (36, default 80 percent) for Protection_Time (35, default 200, meaning 2 seconds), and the servo then holds only Protective_Torque (34, default 20 percent). The datasheet adds the detail that matters at the bench: sending a fresh position command clears the overload flag. An arm that went limp mid-session and then behaves after you re-command it did not fix itself, it latched and got cleared. The independent bench test saw exactly that, with output dropping to roughly 20 percent of nominal under prolonged heavy load.
What lerobot changes, and what it leaves alone
Before you tune anything, know what the stack already did to your servos. lerobot writes settings on every connect, so the values in your motors are not factory values. Vendor default against what the SO follower actually writes at v0.6.1:
| Setting | Vendor default | lerobot v0.6.1 writes | Applies to |
|---|---|---|---|
| Position loop P | 32 | 16 | all six joints |
| Position loop I | 0 | 0 | all six joints |
| Position loop D | 32 | 32 | all six joints |
| Acceleration (addr 41) | 0 | 254, about 2232 degrees per second squared | all six joints |
| Maximum_Acceleration (addr 85) | not listed in memory table V3.7 | 254 | all six joints |
| Return_Delay_Time | table says 0, lerobot comment says 250 | 0 | all six joints |
| Phase (addr 18) bit 4 | 12 | cleared if set, to keep positions in range | sts3215 only |
| Max_Torque_Limit | 1000 (100 percent) | 500 (50 percent) | follower gripper only |
| Protection_Current | 500 (3250 mA) | 250 (1625 mA) | follower gripper only |
| Overload_Torque | 80 | 25 | follower gripper only |
| Torque on disconnect | stays as commanded | disabled (disable_torque_on_disconnect defaults to True) | all six joints |
| max_relative_target | n/a | None, no cap on a single commanded jump | all six joints |
Three rows are worth arguing about. Acceleration 254 is the highest value the register accepts, chosen so teleop feels responsive; every commanded step is taken as fast as the servo can take it, and peak current follows. Halving P from 32 to 16 makes the loop softer, and going further resolved a vibration report on SO-ARM100 issue 24: the user dropped P to 8 on the base and shoulder motors and cut Minimum_Startup_Force (address 24) from 16 to 5, after which the vibration was almost gone. Both are one-line writes to try before suspecting the gearbox.
The third is a documentation problem rather than a setting. lerobot writes 25 to the gripper's Overload_Torque with the source comment "25% torque when overloaded". But address 36 is the threshold that starts the overload countdown; the torque held afterwards is address 34, Protective_Torque, which lerobot does not touch. Read literally, the write lowers the gripper's overload threshold from 80 percent to 25 percent, sensible for the joint most likely to stall, but not what the comment describes. Reported here as a discrepancy between comment and register map, not as a bug.
max_relative_target in the SO follower config defaults to None, which means nothing caps how far a single commanded step may jump from the current position. During teleoperation that is fine, because a human hand cannot produce a step change. During an autonomous rollout it is not: a policy that outputs a bad action sends the joint at it with everything the servo has. Set it to a positive scalar in the config before you run a fresh checkpoint on hardware. This is also the difference between a policy that freezes and one that slams a joint into its end stop.
Reading wear out of a recorded dataset
A teleoperation rig already logs the two signals you need. Every frame in a LeRobot dataset holds an action (where the leader said to go) and an observation.state (where the follower actually was). The gap between them is tracking error, and it grows as a gearbox loses efficiency or a motor loses torque. The measured evidence comes from an independent bench test, run on the 12 V C018 variant rather than the 7.4 V part: mean absolute position deviation was about 22.5 encoder counts (roughly 2 degrees) under a 10 kg.cm dynamic load, rising to about 30 counts (2.6 degrees) at 15 kg.cm. Load and tracking error move together, which is the property the method below leans on.
- 1Pick one dataset and one pose
Use a recording where the arm reaches the same waypoint in every episode. A pick-and-place task with a fixed home pose is ideal. Comparing across tasks tells you nothing.
- 2Check the units before you compute anything
In lerobot v0.6.1 the SO follower's
use_degreesdefaults toTrue, so a fresh recording is in degrees. Older ones may not be: the config dump in lerobot issue 2819, from version 0.4.3, shows'use_degrees': False. Checkmeta/info.jsonrather than assuming, because comparing a degree recording against a normalized one produces a large fake drift.bashpython -c "import json,sys; d=json.load(open(sys.argv[1])); print(d['features']['action'])" meta/info.json - 3Average the per-joint gap for each episode
Take the mean absolute difference per joint per episode.
LeRobotDataset.select_columnsreturns the underlying Hugging Face dataset with only those columns, which keeps the video frames out of memory.pythonimport numpy as np from lerobot.datasets.lerobot_dataset import LeRobotDataset ds = LeRobotDataset("your-user/your-so100-task") names = ds.meta.features["action"]["names"] rows = ds.select_columns(["episode_index", "action", "observation.state"]) sums, counts = {}, {} for row in rows: ep = int(row["episode_index"]) err = np.abs(np.asarray(row["action"], dtype=float) - np.asarray(row["observation.state"], dtype=float)) sums[ep] = sums.get(ep, 0) + err counts[ep] = counts.get(ep, 0) + 1 for ep in sorted(sums): mean = sums[ep] / counts[ep] cells = " ".join(f"{n.split('.')[0]}={v:5.2f}" for n, v in zip(names, mean)) print(f"ep {ep:03d} {cells}") - 4Repeat on a dataset from a month ago
Run the same script against an older recording of the same task on the same arm. You are looking for one joint whose mean gap has grown while the others have not.
bashpython joint_gap.py 2>&1 | tee gap-$(date +%F).txt diff <(cut -c1-80 gap-2026-07-10.txt) <(cut -c1-80 gap-2026-08-24.txt) - 5Confirm on the hardware before you believe it
A growing gap can also be a loosened horn screw, a changed leader arm, or a recalibration in between. Go back to the register script and check whether that joint also runs hotter or draws more current. Two independent signals pointing at one joint is a finding; one is a hypothesis.
Two caveats, both important. First, the leader-follower gap is never zero, because the follower is chasing a moving target and there is real lag in the loop, so the absolute value is meaningless. Only the change over time on the same setup means anything. Second, if you re-ran calibration between the two recordings, the zero moved and the comparison is void. That is not a small footnote: it is the single most common way this analysis produces a confident wrong answer.

A maintenance routine you will actually run
A routine only works if it is short. The failures above are dominated by heat, stalls and screws pulling out of plastic, so those are the three things this checks. Everything here takes minutes, not an afternoon.
| Cadence | Check | What you need | Act when |
|---|---|---|---|
| Every session | Park at the rest pose and disconnect properly rather than cutting power mid-pose | Nothing | Always. lerobot disables torque on disconnect by default, so use it |
| Every session | Run the register script hot, right after the last episode | The script above | Any joint above 50 C, or a non-zero status byte |
| Weekly | Hand-check play at the end effector with power off | Your hands | Play you can feel that was not there last week |
| Weekly | Retighten the M3x6 horn screws and the M2x6 motor screws | The screwdriver set from the bill of materials | Any screw that turns more than a few degrees before biting |
| Weekly | Listen to the arm through one full episode | Nothing; datasheet baseline is 60 +/- 5 dB at 30 cm | One joint is audibly louder than it was, the only early signal for gear damage |
| Monthly | Run the dataset gap script against the current and the previous month's recording | One older dataset of the same task | One joint's mean gap grows while the others hold |
| Per project | Re-run calibration and store the file with the dataset | lerobot-calibrate | After any servo swap, horn removal, or joint that slipped |
Two habits do more than the whole table. Give the arm rest between recording blocks: the independent bench test, again on the 12 V part, measured about 15 C of rise after ten minutes of continuous motion lifting 1.5 kg on a 10 cm lever, which is 15 kg.cm, three times the 7.4 V part's rated torque. Do not expect that number on an SO-100; the point is that heat accumulates across a session and the servo has no fan. And keep spares: the SO-ARM100 bill of materials lists the STS3215 at 13.89 USD or 12.20 EUR per servo, read on 2026-08-24, cheap enough that one spare of each gear ratio in a drawer beats a week of downtime.
- 1Swap the dead servo
Power down, unplug the 3-pin cables on both sides, remove the four M2x6 screws holding the motor and the M3x6 horn screw, and fit the replacement. Match the part number against the leader table above; a follower joint always takes the 1/345 C001.
- 2Give the new servo its bus id and baud rate
A new servo arrives with id 1. Connect only that motor to the board and run the setup, which writes id and baud into EEPROM. The script walks the joints in reverse and prompts for the gripper first, so you can stop after the one you replaced.
bashlerobot-setup-motors \ --robot.type=so101_follower \ --robot.port=/dev/ttyACM0 - 3Recalibrate the whole arm
Not just the joint you touched. Calibration writes a homing offset and a range into every servo, and it is what makes leader and follower agree on what a given angle means.
bashlerobot-calibrate \ --robot.type=so101_follower \ --robot.port=/dev/ttyACM0 \ --robot.id=my_awesome_follower_arm - 4Decide what happens to your old data
Datasets recorded before the swap are referenced to the old zero. If the new offset is close, a fine-tuned policy may cope. If not, you see it as a policy that works in one setup and nowhere else, and no amount of retraining fixes a mislabelled joint origin.
This is the step that eats a day. You replace one servo, recalibrate, and your existing checkpoint now drives the arm to slightly the wrong place, which looks exactly like a training failure. Before you spend another GPU run on it, keep the old calibration file, record five fresh episodes, and compare the resting joint angles against the old dataset. If the origin moved, the fix is a data problem, not a model problem, and the setup-specific failure page walks the same symptom from the other direction.
Do it yourself, or let the platform carry part of it
There is no version of this where somebody else tightens your screws. But much of the work around servo wear is not the wrenching, it is the recording, the comparing and the knowing what healthy looks like, and that part can be shared. Same goal, keeping a rig usable for months, two ways.
- Install lerobot with the Feetech extra (
pip install -e ".[feetech]"), which pulls in thescservo_sdkpackage the bus class requires, then reach every motor withlerobot-find-port. - Save the register script as a file, run it cold and hot, append to a dated log. Ten seconds per session.
- Keep one older dataset per task as a reference recording, and run the gap script against it once a month.
- Keep a low-level fallback for when the Python path is itself broken: FT_SCServo_Debug_Qt, a Qt utility for Feetech SCS and STS servos with prebuilt Ubuntu packages for amd64 and arm64, plus the SCServo Python SDK the status-packet error strings come from.
- Stock spares by part number, not by name: one C001 for any follower joint, and for an SO-101 leader also one C044 and one C046.
- Own the whole loop, including deciding whether a wobble is the gearbox or a screw.
- Full access to every register, including the protection settings the platform never touches
- The health log lives next to your datasets, so hardware history and data history line up
- Nothing depends on network latency or somebody else's uptime
- You build and maintain the tooling, and nobody reviews it
- No baseline from other arms, so you only know your own drift
- The failure diagnosis is entirely yours, including the wrong turns
- Drive a maintained physical arm on the live queue before you own one, with no signup, so you know what a healthy SO-100 feels like when yours starts feeling different.
- Record with the desktop client, which writes LeRobot-format datasets with episodes, camera streams and joint states, so the columns the gap script needs are there without extra work.
- Use the failure-mode pages to separate a worn servo from a loose horn, a serial problem or a camera fault before you touch the hardware.
- Browse the public dataset directory for a comparable recording, or point training at a Hugging Face repo id, so your month-old baseline is not the only reference you have.
- When the data really is fine and the policy really is wrong, train on rented GPUs rather than adding a hardware variable to a software question.
There is no servo telemetry dashboard here. Present_Load, Present_Temperature and the status byte are read over the serial bus by whatever code runs next to the arm, and nothing on this site collects them. The platform hosts the datasets that reveal a trend and the pages that name the failure mode. It cannot tell you a gear is worn, and it definitely cannot tighten an M3 screw.
Where none of this helps
Being straight about the limits. Servo wear that has already crossed into failure is not detectable earlier by better software, because the STS3215 has no wear sensor. It has a thermometer, an ammeter and a duty-cycle estimate, and all three report load right now, not the state of the gear teeth. If the failure mode is a chipped tooth, the first signal you get is the noise it makes, which is why listening is on the weekly list.
The second limit is remote work. Teleoperating across the internet is genuinely useful for recording demonstrations and it is how a lot of operator work gets done, but a remote operator cannot hear a gearbox or feel play at the end effector. Nor does remote inference fix a hardware problem: control loops run between 20 ms and 485 ms per action step depending on the model, and adding public-internet round trips to that turns a working policy hesitant. That is a latency limit, not a wear limit, and no amount of maintenance changes it.
- Thermal problems, well before the 70 C limit that the bench test suggests may not enforce itself
- A joint working harder for the same motion, weeks before it stalls
- Latched protection events you would otherwise never notice
- Supply problems, since Present_Voltage is on the same bus read
- Mechanical play, which needs a hand and a dial indicator, not a register
- Cracked printed parts and screws pulling out of plastic
- Damaged gear teeth, which read as normal until they do not
- Cable and connector fatigue, which shows up as read errors rather than wear

Where to go next
If you are building the rig rather than maintaining it yet, the complete SO-100 setup guide covers assembly and first teleoperation, and the SO-100 against SO-101 comparison explains why the newer arm dropped the gear-removal step and re-geared the leader. If your problem is that the data is bad rather than the hardware, the data quality guide and the recording tutorial are the better starting points. And if a specific joint is misbehaving right now, joint stops early, arm twitches then sags and gripper does not close each start from the symptom instead of the theory.
When the arm is the bug, not the policy
The failure-mode pages start from what you can see: a joint that stops short, a gripper that will not close, an arm that twitches then sags. Each one separates the hardware causes from the calibration and data causes before you spend another GPU run on a model that was never wrong.
Open the fix indexFrequently asked questions
How long does an STS3215 last on an SO-100?▾
There is no honest single number. Feetech's published reliability test is more than 100,000 cycles of a 1.5 second motion at one fifth of stall torque, about 42 hours of continuous motion under that specific light load. Your arm runs a different load at a different acceleration in a different pose, so use it as an order of magnitude: tens of hours of motion, not thousands. Joints that stall regularly, above all the gripper, fall short of it; joints that mostly hold still exceed it.
Which joint should I keep a spare for?▾
The gripper first, then shoulder_lift. The gripper because stalling it against an object is how it is used, and a lerobot maintainer identified prolonged overload as the mechanism when a follower servo died on issue 2819. shoulder_lift because it carries the arm's weight for the whole session including while parked, so it runs hottest. Order by part number: every follower joint takes the 1/345 C001, while an SO-101 leader mixes C044, C001 and C046 across its six joints.
Is a warm servo a problem?▾
Warm is normal, hot is a warning. The datasheet gives an operating range of -20 C to 60 C, and the factory Max_Temperature_Limit at address 13 is 70 C, above which the datasheet says the servo closes its torque output. Treat that as a limit, not a safety net: an independent bench test on the 12 V variant drove a unit to 71 C without observing any thermal shutdown and concluded a manual cutoff is needed. The same test measured roughly 15 C of rise after ten minutes of continuous motion at 15 kg.cm, three times the 7.4 V part's rated torque. What matters on your arm is the comparison between joints: if five servos sit near ambient and one is 20 C above them, that one is doing work the others are not.
My arm developed play. Is that the gears?▾
Usually not, at least not first. Check screws before gears: the M3x6 horn screws and the M2x6 motor screws thread into printed plastic, and they loosen. The datasheet specifies gear backlash at 0.5 degrees or less; an independent test on the 12 V variant measured 1.3 mm of movement at the tip of an 86 mm lever with a dial indicator, which is about 0.87 degrees of total angular play, well above the specification even on a healthy unit. Tighten everything, then re-measure. If the play returns within a week the screw hole is worn, not the gearbox.
Can I turn Present_Current into a torque figure?▾
Approximately, yes, and it is the most useful thing you can do with these registers. Present_Current at address 69 counts in units of 6.5 mA, and the datasheet gives a Kt of 7.8 kg.cm per amp, so counts times 0.0065 times 7.8 gives kg.cm. The datasheet's own figures agree: 650 mA rated current gives 5.07 kg.cm against a 5 kg.cm rated torque, and 2.5 A stall current gives exactly the quoted 19.5 kg.cm. It ignores gearbox efficiency losses, so use it to compare a joint against itself over time, not as a calibrated measurement.
Do I have to recalibrate after replacing one servo?▾
Yes, and the whole arm rather than the one joint. A new servo arrives with bus id 1 and factory settings, so it needs lerobot-setup-motors to write id and baud rate to EEPROM, then lerobot-calibrate for homing offsets and ranges. The consequence people miss is downstream: datasets recorded before the swap sit on the old zero, and a policy trained on them may drive the arm to slightly the wrong place afterwards.
Sources
- Feetech STS3215 product specification, edition A/1, 2023-06-23: torque, current, Kt, life test and electronic protection
- ST3215 memory table V3.7: register addresses, units and factory initial values
- TheRobotStudio/SO-ARM100: bill of materials, C001/C044/C046 servo variants, 12 V option and the SO-101 changes
- LeRobot SO-101 assembly guide: leader gear ratio per joint, screw sizes, setup-motors and calibrate commands
- lerobot v0.6.1 SOFollower.configure: PID writes and the gripper-only torque and current limits
- lerobot v0.6.1 SOFollowerConfig: P/I/D defaults, max_relative_target, disable_torque_on_disconnect, use_degrees
- lerobot v0.6.1 FeetechMotorsBus: configure_motors return delay and acceleration, sign decoding, normalized data
- lerobot v0.6.1 MotorsBus.read and disconnect: the raise_on_error path and torque-off default
- lerobot v0.6.1 Feetech control table: register addresses and the Present_Load sign bit
- lerobot v0.6.1 LeRobotDataset: meta.features and select_columns
- lerobot issue 2819: SO-101 follower gripper servo broken, maintainer names prolonged overload
- SO-ARM100 issue 24: base and shoulder vibration resolved with P coefficient 8 and minimum starting force 5
- Independent STS3215 bench test on the 12 V C018 variant: backlash, repeatability, torque and thermal behaviour
- Feetech SCServo Python SDK: ERRBIT status-packet error constants in protocol_packet_handler.py
- FT_SCServo_Debug_Qt: Qt utility for Feetech SCS and STS servos, Ubuntu deb packages for amd64 and arm64
Sources
- Feetech STS3215 product specification, edition A/1, 2023-06-23: torque, current, Kt, life test and electronic protection
- ST3215 memory table V3.7: register addresses, units and factory initial values
- TheRobotStudio/SO-ARM100: bill of materials, C001/C044/C046 servo variants, 12 V option and the SO-101 changes
- LeRobot SO-101 assembly guide: leader gear ratio per joint, screw sizes, setup-motors and calibrate commands
- lerobot v0.6.1 SOFollower.configure: PID writes and the gripper-only torque and current limits
- lerobot v0.6.1 SOFollowerConfig: P/I/D defaults, max_relative_target, disable_torque_on_disconnect, use_degrees
- lerobot v0.6.1 FeetechMotorsBus: configure_motors return delay and acceleration, sign decoding, normalized data
- lerobot v0.6.1 MotorsBus.read and disconnect: the raise_on_error path and torque-off default
- lerobot v0.6.1 Feetech control table: register addresses and the Present_Load sign bit
- lerobot v0.6.1 LeRobotDataset: meta.features and select_columns
- lerobot issue 2819: SO-101 follower gripper servo broken, maintainer names prolonged overload
- SO-ARM100 issue 24: base and shoulder vibration resolved with P coefficient 8 and minimum starting force 5
- Independent STS3215 bench test on the 12 V C018 variant: backlash, repeatability, torque and thermal behaviour
- Feetech SCServo Python SDK: ERRBIT status-packet error constants in protocol_packet_handler.py
- FT_SCServo_Debug_Qt: Qt utility for Feetech SCS and STS servos, Ubuntu deb packages for amd64 and arm64
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started