Skip to content

fix: bound the calibration probe and surface its errors - #14

Merged
yd-sl merged 1 commit into
mainfrom
fix/bound-calibration-probe
Sep 29, 2026
Merged

yd-sl merged 1 commit into
mainfrom
fix/bound-calibration-probe

Conversation

@yd-sl

@yd-sl yd-sl commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Three calibration-safety fixes with one theme: the routine that drives the jaws into a hard stop, and the calibration files it reads and writes, must not damage the gripper and must not hide a mistake.

The probe used to advance its target unconditionally (target += sign * step_rad). Once the jaws reached a stop the command kept leading further every cycle, so kp * error kept growing until the structure gave way. The position-based stall test cannot stop that on hardware: at a stop the encoder still creeps (backlash, elastic deformation, micro-slip), so delta < stall_delta never holds. Two independent guards now bound it. This is the failure mode that broke a jaw during calibration on 2026-09-29.

The other two fixes make a calibration mistake visible instead of silent: an unknown motor id written as 0 bricked the next enable(), and the package's own NullHandler swallowed the warning that a calibration file belongs to another channel.

Bundled in one PR because all three are "calibration must be safe and honest"; each is a fix: so the release is a single patch. Splitting is easy if you prefer separate PRs.

Changes

  • src/litegrip/gripper.py — calibrate() / calibrate_guided(): re-derive the probe target from the measured position each cycle (lead <= step_rad), so the pressing torque is at most kp * step_rad; add tau_limit (default 2.0 Nm) and abort the probe the moment |tau| reaches it. Replace calibrate()'s two half-second unguarded goto_rad(..., kp=80) back-offs with a _bounded_move that uses the same lead and torque guards. Probe defaults move to kp=20, step_rad=0.05, stall_delta=0.0015, stall_cycles=5, max_iter=200 — the values zero() already used.
  • src/litegrip/gripper.py — save_calibration() omits mst_id while the id is unknown instead of writing 0; load_calibration() treats a falsy can_id / mst_id as "unknown" so it keeps auto-detect rather than pinning the RX filter to 0x000.
  • src/litegrip/actions.py — the MotionConfig probe defaults above, plus calib_tau_limit; GripperActions.zero() passes it through.
  • src/litegrip/__init__.py — drop the NullHandler (logging.lastResort now shows WARNING and above) with a comment saying why.
  • tests/test_calibration.py — TestCalibrateCommandLeadIsBounded, TestCalibrateTorqueCeiling (a fake motor that keeps creeping at the stop, so only the torque ceiling can end the probe), TestMasterIdIsNotPinned, TestLibraryDoesNotSilenceItsOwnLogs.
  • README.md / readme_zn.md — updated calib_* table rows and the zero() vs calibrate() note.

Testing

$ cd /home/qql/sl/litegrip-python && python3 -m unittest discover -s tests -t tests
Ran 115 tests in 1.770s

OK

$ npx --yes markdownlint-cli2@0.23.3 "README.md" "readme_zn.md"
Summary: 0 issues in 0 files

The probe guard is covered by the fake-motor suite only. The tau_limit abort was not re-run on real hardware in this PR; it is a new ceiling on top of the lead bound, so the tested behaviour is at least as safe as the validated one.

Issues

None — the repository has no issue tracker entries, matching the previous PRs.

Three calibration-safety fixes on one theme: the routine that drives the
jaws into a hard stop, and the calibration files it reads and writes, must
not damage the gripper and must not hide a mistake.

The probe advanced its target unconditionally (`target += sign * step_rad`),
so once the jaws reached a stop the command kept leading further every cycle
and `kp * error` kept growing until the structure gave way. The
position-based stall test cannot catch that: at a stop the encoder still
creeps (backlash, elastic deformation, micro-slip). The target is now
re-derived from the measured position each cycle, capping the lead at one
step (pressing torque <= kp * step_rad), and the probe aborts the moment
`|tau|` reaches the new `tau_limit` (2.0 Nm) -- a guard that does not depend
on the stall counter. `calibrate()`'s two back-offs get the same treatment
instead of a half-second unguarded `goto_rad(..., kp=80)`. Probe defaults
move to kp=20 / step=0.05, the values `zero()` already used.

`save_calibration` stamped `"mst_id": 0` when the id was unknown. Reading
that file back installed a CAN RX filter of 0x000, so every motor reply was
dropped and `enable()` failed after its whole retry loop. The key is now
omitted while unknown, and a falsy id in an existing file keeps auto-detect.

The package installed a `NullHandler`, hiding its own WARNING that a
calibration file belongs to another channel -- a safety signal the user has
to see. With no handler, `logging.lastResort` prints it to stderr.
@yd-sl
yd-sl merged commit ea99bdc into main Sep 29, 2026
1 check passed
@yd-sl
yd-sl deleted the fix/bound-calibration-probe branch September 29, 2026 07:45
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.5.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant