fix(gentle): a corrupt Torque_Limit read must not raise from the cleanup#42
Conversation
…nup (v0.22.1)
Found on hardware while probing joint limits.
read_torque_limit returned 2048 on a healthy motor whose register actually held
500. The read SUCCEEDED — it just returned nonsense, and 2048 is outside the
register's own [0, 1000] range, so it cannot be a torque limit at all. The bus
layer retries FAILED reads; it has no way to know a successful one is garbage.
The value was stashed as the pre-move Torque_Limit. The finally then tried to
write it back, the servo's range check rejected it, and a CliError was raised OUT
OF THE CLEANUP — masking whatever the move had actually been doing. The finally
suppressed only OverloadError, so a CliError sailed straight out of it.
This is the third instance of the same trap (safety._release_motor and
profile._release were the first two), and the second instance of the deeper one: a
read that succeeds and lies. A plausible-looking wrong value is more dangerous than
an error, because nothing downstream can tell it from a real reading — exactly like
the read_position that returned 0 immediately after an EEPROM write.
Two fixes:
- VALIDATE AT THE READ, not at the write. _sane_torque_limit() returns None for
anything outside [0, 1000]. If we do not know the pre-move value we say so,
rather than restoring a fiction.
- The finally CANNOT RAISE. With the pre-move value unknown, the conservative
_CONTACT_TORQUE_LIMIT cap simply stays in place. A joint left slightly
under-torqued is a nuisance; a cleanup that raises is a lie about why the move
failed.
Two regression tests, both proven RED against the old code and GREEN against the
fix: the move still reports itself, and 2048 never reaches the servo.
1244 tests pass (was 1242).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vrVNSJ9kzhCQqwpA2e8u6
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018vrVNSJ9kzhCQqwpA2e8u6
|
/agentic_review |
PR Summary by QodoFix gentle_move: ignore corrupt Torque_Limit reads and never raise from cleanup
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1. Silent torque restore failure
|
…ge 63.6% -> gate)
SonarCloud failed the gate on new-code coverage (63.6% vs 80%) with ZERO issues —
and it was right to. The uncovered lines were the except branches of the restore:
the exact safety property this PR adds. Shipping 'the cleanup can no longer raise'
without a test that MAKES it raise is shipping a claim, not a guarantee.
Two tests, mirroring the asymmetry:
- a bus that accepts the CAP but fails the RESTORE (the normal case — the restore
runs against a bus that may have just died, which is the whole reason it exists):
the move still completes and reports where it ended.
- a restore that raises SystemExit: it PROPAGATES. Everything else from the cleanup
is swallowed — a dead bus, a corrupt packet, a second Ctrl-C — but an explicit
request for the interpreter to exit is never this module's to override. Same rule
as safety._release_motor, now pinned so it reads as a choice rather than an
oversight.
gentle.py coverage 96% -> 98%. 1246 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vrVNSJ9kzhCQqwpA2e8u6
|



Found on hardware, mid-probe, while mapping joint limits.
The bug
read_torque_limitreturned 2048 on a healthy motor whose register actually held 500.The read succeeded — it just returned nonsense, and 2048 is outside the register's own
[0, 1000]band, so it cannot be a torque limit at all.That value was stashed as the pre-move
Torque_Limit. Thefinallythen tried to write itback, the servo's range check rejected it, and a
CliErrorwas raised out of the cleanup —masking whatever the move had actually been doing.
The
finallysuppressed onlyOverloadError, so aCliErrorsailed straight out of it.Two things this is an instance of
A cleanup that can raise. Third time:
safety._release_motorandprofile._releasewere thefirst two. The rule is now uniform — cleanup survives any
BaseException(withSystemExitre-raised), because a cleanup that throws replaces the failure the operator actually needs to see.
A read that succeeds and lies. Second time today: a
read_positionreturned0immediatelyafter an EEPROM write while the servo genuinely held 3387. The bus layer retries reads that
fail; it has no way to know a successful one is garbage. A plausible-looking wrong value is
more dangerous than an error, because nothing downstream can tell it from a real reading.
The fix
_sane_torque_limit()returnsNonefor anything outside
[0, 1000]. If we do not know the pre-move value, we say so rather thanrestoring a fiction.
finallycannot raise. With the pre-move value unknown, the conservative_CONTACT_TORQUE_LIMITcap simply stays in place. A joint left slightly under-torqued is anuisance; a cleanup that raises is a lie about why the move failed.
Tests
Two regression tests, both verified RED against the old code and GREEN against the fix:
finally2048never reaches the servo — only the cap is written1244 tests pass (was 1242). black / isort / flake8 / bandit clean.