Incident report · Unitree A1 · DreamWaQ deploy
The robot jammed a calf against a curb, the policy commanded a large extension into the pinned joint, and the motor spent 0.1 s at 37 N·m without moving. The driver cut it. A second calf failed 9.5 s later by a different route — and the existing stall watchdog was structurally unable to see either.
Nine control steps. The calf runs to its mechanical limit and stops moving; the policy then commands roughly 2.4 rad of extension into a joint that cannot move; torque climbs to 37 N·m and stays there for five consecutive samples with velocity at essentially zero. That is a stall, and it is what killed the motor.
| t (s) | q (rad) | qdes (rad) | tau (N·m) | dq (rad/s) | state |
|---|---|---|---|---|---|
| 271.06 | −2.7058 | −2.7000 | +0.89 | −0.75 | calf reaches its −2.69653 stop, motion ends |
| 271.08 | −2.7062 | −2.3581 | +0.00 | +0.22 | target begins a large extension |
| 271.12 | −2.4920 | −0.3000 | +6.00 | +16.26 | target now 2.4 rad away |
| 271.14 | −2.3199 | −0.3596 | +24.42 | −5.80 | torque ramps into a pinned joint |
| 271.16 | −2.3662 | −0.9059 | +33.41 | −0.37 | stalled — over rating, not moving |
| 271.18 | −2.3756 | −0.7996 | +37.00 | −0.63 | stalled |
| 271.20 | −2.3772 | −0.8849 | +36.86 | +0.19 | a per-joint check would fire here |
| 271.22 | −2.3731 | −0.8281 | +36.82 | +0.23 | stalled |
| 271.24 | −2.3655 | −0.9653 | +36.61 | +0.68 | stalled |
| 271.26 | −2.4010 | −1.0297 | +8.10 | −3.91 | driver protection cuts the motor |
Five samples above 33 N·m with |dq| never exceeding 0.68 rad/s — high current, no work. The rated limit is 33.5 N·m. After t = 271.26 the joint produces nothing.
What failed
RR_calf stopped producing torque at t = 271.26 s and never recovered during the run. RL_calf followed at t ≈ 280.8 s, its torque becoming intermittent from 280.2 and effectively zero from 280.84, with the joint drifting to −2.70 rad and hanging there.
Throughout, the robot stayed upright — grav_z between −0.88 and −0.99 — and the controller phase never left RUN. Nothing tripped and nothing aborted. That is why the operator experienced a joint locking up mid-walk rather than a fall.
Both motors recovered by run0043 (RR_calf back to a 5.71 N·m mean), so the protection latch clears on a power cycle.
The cause
The operator has video: the robot was climbing a curb and slammed the edge. At t = 270–271 the attitude goes from level to pitch −0.438 rad (25°) in about a second, roll swings to +0.24, and foot forces sit near saturation at roughly 950 raw.
But the impact is not what killed the motor. The impact left the calf pinned — run to its −2.69653 rad mechanical stop with the foot jammed against the curb, velocity at zero. What killed it was the next 0.1 s: the policy, seeing a leg far from where it wanted it, commanded roughly 2.4 rad of extension. Into a joint that could not move, that command becomes pure current. Torque held between 33.4 and 37.0 N·m across five consecutive control steps while |dq| never exceeded 0.68 rad/s, and the driver's protection latched.
The distinction matters for the fix. An impact is unpredictable and brief; a stall is a sustained, unambiguous signature — high torque with near-zero velocity — and it is detectable within two or three control steps.
The second failure
When RR_calf dropped out, the remaining legs absorbed its share. RL_calf, already the second most loaded joint on the robot, then took repeated overloads over about six seconds rather than one stall:
| t (s) | mean |tau| | peak |tau| | vs 33.5 rating |
|---|---|---|---|
| 277 | 8.03 | 36.89 | over |
| 278 | 8.98 | 34.98 | over |
| 279 | 13.96 | 37.43 | over |
| 280 | 1.91 | 32.35 | near |
| 281 | 0.26 | 3.02 | dead |
Four excursions to or past the rating across four seconds, with the mean load climbing steadily, and then it quits. RR was an instantaneous stall trip; RL was accumulated abuse. A guard that catches only one of these catches half the incident.
Why the rear calves
What separates survivors from casualties is how close each motor already was to its limit. Measured over the healthy portion of the run — 199 s of driving before t = 270:
| Joint | Time > 20 N·m | Outcome |
|---|---|---|
| FL_calf | 0.6% | survived |
| FR_calf | 0.9% | survived |
| RL_calf | 3.6% | died |
| RR_calf | 4.3% | died |
The rear calves carry three to four times more high-torque time than the front pair. They had the least margin left, and they are exactly the two that failed. For reference, FR_calf also peaked at 37.04 N·m during the same impact — the highest of any joint — and survived it, because it was not stalled and had margin.
Why nothing caught it
deploy_safety.h already contains a StallWatch, and it did not fire. It is not broken — it is aimed at a different failure. It trips on whole-robot sums: Σ|tau| > 60 N·m and Σ|tau·ω| < 15 W, held for 1.0 s. That describes a robot pinned down bodily — every joint straining, nothing moving.
During this stall, one joint was pinned and eleven were walking.
| t (s) | Σ|tau| (> 60?) | Σ|tau·ω| (< 15 W?) | RR_calf alone |
|---|---|---|---|
| 271.16 | 86.14 ✓ | 234.1 W ✗ | 12.4 W |
| 271.18 | 97.67 ✓ | 306.9 W ✗ | 23.3 W |
| 271.20 | 102.60 ✓ | 369.6 W ✗ | 7.1 W |
| 271.22 | 94.28 ✓ | 314.7 W ✗ | 8.6 W |
| 271.24 | 113.90 ✓ | 408.8 W ✗ | 24.9 W |
The torque condition was satisfied at every step. The mechanical-power condition was never close: the stalled joint contributed 7–25 W and drowned in the 300+ W the other eleven joints were legitimately producing. The 1.0 s hold would have missed a 0.1 s stall regardless.
One useful by-product: deploy_safety.h notes beside mech_thresh that the walking value of Σ|tau·ω| was unmeasured, and warns not to treat 15.0 as calibrated. This incident measures it — 234–409 W during normal locomotion, more than twenty times the threshold.
Trip on |taui| > ~30 N·m and |ωi| < ~1 rad/s for three consecutive control steps, evaluated per joint rather than summed. Applied to this trace it fires at t = 271.20 — 60 ms before the driver cut the motor at 271.26.
Sixty milliseconds may or may not beat the driver's own protection for the first motor; that is worth testing rather than assuming. But it is ample for the cascade. RL_calf took roughly six seconds of repeated overload before it quit, so the same check would comfortably have saved the second motor — leaving the robot limping on eleven working joints instead of ten.
This replaces an earlier proposal for a torque-spike counter. You cannot predict the impact, and you do not need to: the impact is not what kills the motor. The 0.1 s stall afterwards is, and a stall is unambiguous within two or three control steps.
Correction — supersedes an earlier diagnosis
An earlier reading of this incident blamed chronic end-stop hammering: calves driven repeatedly into their −2.69653 rad limit across the whole run until a motor gave out. The evidence looked strong — samples with the joint past its stop counted RR 614, RL 280, FR 75, FL 24, a perfect rank correlation with who died.
That correlation does not exist. Roughly 95% of those samples occur after the motor lost torque, when the dead leg hangs past the stop under its own weight. Per 2.5-second bucket the count runs 0–5 right up to t = 272.5 — after the cutout — and only then jumps to 125/125, which is just the frozen joint being counted over and over.
Recomputed on healthy data only (t < 270): FL 20, FR 72, RL 20, RR 35. FR_calf hammered its stop most and survived.
The calf did reach its stop at t = 271.06, immediately before the stall — that single contact is part of the jam that set the stall up. What was wrong was the claim that run-long, repetitive stop contact caused the failure, and the statistic that appeared to support it.
The general lesson: a statistic computed over the whole episode included the aftermath of the event it was meant to explain. The contamination was not a rounding concern — it was the overwhelming majority of the signal, and it pointed confidently at the wrong cause.
Ruled out
The deploy runs PowerProtect at level 5 — per the SDK header, levels 1–10 mean a 10%–100% power limit, so L5 is a 50% limit. Not responsible, for three independent reasons:
LowCmd, so it cannot single out one motor of twelve.It is also the wrong kind of lever. PowerProtect limits what you command; a stall is created by the world holding the joint still while a legitimate command is applied.
Heat remains credible as a predisposing factor. run0042 carried the highest calf torques of the day — mean 5.64–6.14 N·m across all four, against 3.1–5.5 typical — and it was the third consecutive long run (230 s, 141 s, 215 s of driving). A warm motor has less margin before its protection latches, and the same stall on a cold robot might have been survivable.
But this cannot be confirmed. motorState.temperature was never logged. It exists in the SDK and reads 34–41 °C at rest, but was not recorded during any of these runs. Thermal contribution is unmeasured, not demonstrated.
Nuance
tauEst is estimated joint torque, and on a hard impact it includes torque applied from outside the robot — the curb back-driving the leg — not only what the motor chose to produce. During the stall specifically, the picture is simpler and worse: the joint is not moving, so the torque is very largely the motor's own effort against a fixed obstruction. That is precisely the condition that heats a winding fastest, because none of the electrical power is leaving as mechanical work.
Actions
Relevance
Simulation measured that the recovery policy holds the calf against this same −2.69653 rad stop for up to 18 seconds continuous at Kp 28. In MuJoCo a joint stop is free; on the robot it is stall torque through a real winding — and run0042 shows that 0.1 s of it is enough to lose a motor.
A recovery attempt is precisely the regime this incident describes: a calf held hard against its limit for many seconds, with no gait to unload it and no other joints doing work to mask it. The per-joint stall check and the thermal gate in dwq_recover.cpp are therefore not optional, and the per-joint form is the one that matters — during a recovery the whole-robot sums would be even less informative than they were here.