The AY-Robots SO-100 hub page, the entry point for the build, teleoperation and training guides for the SO-100 arm.
SO-100Hardware maintenanceFeetech STS3215TeleoperationData collection

Servo Wear and SO-100 Arm Maintenance: What Breaks First

AY-Robots ResearchAugust 23, 202628 min read

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.

SpecificationSTS3215 at 7.4 VNote
Stall torque19.5 kg.cm16.5 kg.cm at 6 V
Rated torque5 kg.cm4 kg.cm at 6 V; the load it is built to carry continuously
Stall current2.5 A2 A at 6 V
Rated current650 mAat rated torque; 150 mA no-load, 21 mA idle
Torque constant Kt7.8 kg.cm per amp2.5 A x 7.8 = 19.5 kg.cm, consistent with stall torque
Rated input voltage4 V to 7.4 Vprotection trips above 8 V or below 4 V, and clears itself
Gear ratio1/345copper gears, PA+GF case, ball bearings, 55 g
Encoder12 bit magnetic, 4096 steps0.088 degrees per step, and the datasheet lists sensor life as no limit
Backlash0.5 degrees or lessa specification, not a measurement of your unit
Operating temperature-20 C to 60 Cstorage -30 C to 80 C
Noise60 +/- 5 dB at 30 cmthe baseline a gearbox gets louder than
Life testmore than 100,000 cycles60 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.

How to read the 42 hours

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 wearsFirst symptomWhere it shows up
gripper (6)Stalling against an object is the intended use, and the leader keeps commanding closure after contactGrip force falls; objects slip late in an episode, not at pickupPresent_Load pinned high while position barely changes
shoulder_lift (2)Holds the arm against gravity all session, including while parkedSag at the rest pose, warm case after a long blockHighest 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 episodeOvershoot and hunting instead of settlingRepeated position error at the same waypoint
shoulder_pan (1)Low gravity load, but it swings the whole arm's inertiaVibration when the arm is extendedOscillation visible in observation.state while nothing is commanded
wrist_flex (4), wrist_roll (5)Light loads, but many small reversals per episodePlay you can feel by hand at the end effectorHysteresis: different resting position depending on approach direction
Printed parts and screwsM2x6 motor and M3x6 horn screws pulling into plastic over many load cyclesPlay that feels exactly like servo backlash but is notDisappears 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.

Two ways to kill six servos in an afternoon

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 ratioPart numberCount in the leader
Base / shoulder pan (1)1/191C0442 with elbow flex
Shoulder lift (2)1/345C0011
Elbow flex (3)1/191C044see above
Wrist flex (4)1/147C0463 with wrist roll and gripper
Wrist roll (5)1/147C046see above
Gripper (6)1/147C046see 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.

RegisterAddressUnitFactory valueWhat a worrying value looks like
Present_Temperature63degrees Cread-onlyAbove 50 C on one joint while the others sit near ambient
Present_Load600.1 percent, sign bit 10read-onlyNon-zero and rising at a standstill pose that used to read near zero
Present_Current696.5 mA per countread-onlySteady draw climbing week over week for the same motion
Status65bit flagsread-onlyAny non-zero value; bit 5 (overload) and bit 2 (temperature) are the ones that matter here
Max_Temperature_Limit13degrees C70Do not raise it. If you are tempted, the joint is telling you something
Max_Torque_Limit160.1 percent of stall1000 (100 percent)Copied into Torque_Limit (48) at power-up
Protection_Current286.5 mA per count500 (3250 mA)The over-current trip point, but see the conflict below
Overload_Torque36percent of max torque80Threshold that starts the overload countdown
Protection_Time3510 ms per count200 (2 s)Time above the threshold before protection engages, 2.5 s maximum
Protective_Torque34percent of max torque20Output torque held after protection engages
Unloading_Condition19bit flags44Which 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.

python
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
A health check against lerobot v0.6.1. Run it with the arm powered and parked at its rest pose, and again right after a recording block. Only Goal_Position and Present_Position are normalized in this version, so these reads come back raw without passing normalize=False.

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 read that refuses to happen

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.

Decoding the status byte, and who to believe

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.

QuestionOne document saysThe other saysWhat to do
Over-current trip pointDatasheet: output shuts off after 2 s above 2 AMemory table: Protection_Current default 500, which is 3250 mARead the register on your own motor rather than assuming either
Return_Delay_Time defaultMemory table: initial value 0lerobot source comment: motors ship at 250, meaning 500 usDoes not matter in practice, because lerobot writes 0 on every connect
Status bit 1Memory table: sensor error, angle on bit 4SCServo SDK: ERRBIT_ANGLE = 2, no bit 4 constantUse the memory table names and expect SDK messages to differ
Thermal cut-off at 70 CDatasheet: torque output closes above 70 CBench run on the 12 V part hit 71 C with no shutdown observed, and recommends a manual cutoffDo not rely on the servo saving itself; watch Present_Temperature
The one thing both documents agree on

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:

SettingVendor defaultlerobot v0.6.1 writesApplies to
Position loop P3216all six joints
Position loop I00all six joints
Position loop D3232all six joints
Acceleration (addr 41)0254, about 2232 degrees per second squaredall six joints
Maximum_Acceleration (addr 85)not listed in memory table V3.7254all six joints
Return_Delay_Timetable says 0, lerobot comment says 2500all six joints
Phase (addr 18) bit 412cleared if set, to keep positions in rangests3215 only
Max_Torque_Limit1000 (100 percent)500 (50 percent)follower gripper only
Protection_Current500 (3250 mA)250 (1625 mA)follower gripper only
Overload_Torque8025follower gripper only
Torque on disconnectstays as commandeddisabled (disable_torque_on_disconnect defaults to True)all six joints
max_relative_targetn/aNone, no cap on a single commanded jumpall 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.

The knob that is not there by default

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.

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

  2. 2
    Check the units before you compute anything

    In lerobot v0.6.1 the SO follower's use_degrees defaults to True, 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. Check meta/info.json rather than assuming, because comparing a degree recording against a normalized one produces a large fake drift.

    bash
    python -c "import json,sys; d=json.load(open(sys.argv[1])); print(d['features']['action'])" meta/info.json
  3. 3
    Average the per-joint gap for each episode

    Take the mean absolute difference per joint per episode. LeRobotDataset.select_columns returns the underlying Hugging Face dataset with only those columns, which keeps the video frames out of memory.

    python
    import 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}")
    
  4. 4
    Repeat 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.

    bash
    python 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)
    
  5. 5
    Confirm 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.

The AY-Robots fix index listing failure modes such as servo not responding, arm twitches then sags, joint stops early and gripper does not close.
The failure-mode index. Most of what looks like servo wear turns out to be one of these, which is why the hardware check comes before the retraining.

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.

CadenceCheckWhat you needAct when
Every sessionPark at the rest pose and disconnect properly rather than cutting power mid-poseNothingAlways. lerobot disables torque on disconnect by default, so use it
Every sessionRun the register script hot, right after the last episodeThe script aboveAny joint above 50 C, or a non-zero status byte
WeeklyHand-check play at the end effector with power offYour handsPlay you can feel that was not there last week
WeeklyRetighten the M3x6 horn screws and the M2x6 motor screwsThe screwdriver set from the bill of materialsAny screw that turns more than a few degrees before biting
WeeklyListen to the arm through one full episodeNothing; datasheet baseline is 60 +/- 5 dB at 30 cmOne joint is audibly louder than it was, the only early signal for gear damage
MonthlyRun the dataset gap script against the current and the previous month's recordingOne older dataset of the same taskOne joint's mean gap grows while the others hold
Per projectRe-run calibration and store the file with the datasetlerobot-calibrateAfter 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.

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

  2. 2
    Give 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.

    bash
    lerobot-setup-motors \
        --robot.type=so101_follower \
        --robot.port=/dev/ttyACM0
    
  3. 3
    Recalibrate 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.

    bash
    lerobot-calibrate \
        --robot.type=so101_follower \
        --robot.port=/dev/ttyACM0 \
        --robot.id=my_awesome_follower_arm
    
  4. 4
    Decide 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.

The recalibration trap

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 the scservo_sdk package the bus class requires, then reach every motor with lerobot-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.
Advantages
  • 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
Trade-offs
  • 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

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.

Monitoring servo health from the registers
What it catches
  • 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
What it misses
  • 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
The AY-Robots teleoperator page with the headline Become a Robot Operator from anywhere in the world, showing a photo of an SO-100 arm.
Remote operators drive arms they never touch. That works for recording demonstrations, and it is exactly why somebody local still has to own the maintenance.

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 index

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

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started