Skip to content

fix: report a failed send with its cause and its type - #67

Merged
cao-xiao-hao merged 1 commit into
mainfrom
fix/a-failed-send-names-its-cause
Oct 14, 2026
Merged

cao-xiao-hao merged 1 commit into
mainfrom
fix/a-failed-send-names-its-cause

Conversation

@cao-xiao-hao

Copy link
Copy Markdown
Contributor

Summary

A failed MIT frame now reports the cause and the type it actually has, instead of one fixed message that guessed.

GripperActions._emit() is the chokepoint for every frame the engine sends, and it turned a failed send_mit_frame() into CommandError("MIT 帧下发失败(未连接或未使能)"). On the bench this shipped a lie: the CAN interface went ENETDOWN part-way through a move, is_connected and is_enabled were both True at the instant the exception was raised, and the real cause — OSError(100, 'Network is down') — existed only as a log.warning attached to no exception, so a caller with no log handler saw the false message and nothing else.

README.md's error table already says what these are: a CAN write that fails mid-session is CommError, and not connected / not enabled is what NotInitializedError exists for. CommandError is for a rejected command. The code now matches the table.

Note for callers: this breaks catching CommandError around a motion call — such a caller now sees CommError or NotInitializedError instead. Both are LiteGripError subclasses, so catching the base still works. Publishes as a patch under this policy.

Changes

  • src/litegrip/protocols/can_bus.py — LiteGripCAN.control_mit() / control_mit_stream() keep the failure in a new last_send_error property (the exception itself, type included; cleared on success, NotInitializedError when nothing is registered). The False return and the warning are unchanged.
  • src/litegrip/gripper.py — new LiteGrip.last_send_error. send_mit_frame() passes the bus reason up, and supplies its own NotInitializedError when _can is None or the gripper is not enabled; the two messages are now single-sourced with _check_connected() / _check_enabled().
  • src/litegrip/actions.py — _emit() raises NotInitializedError for a missing connection or enable, CommError carrying the reason for a transport failure, and says "原因未记录" when nothing recorded one. The original exception rides along as __cause__.
  • src/litegrip/trajectory.py — the replay abort keeps its TrajectoryError type but carries the send's own reason instead of "未连接或未使能?".
  • src/litegrip/teleop.py — the slave loop's first-failure warning names the reason rather than listing both possibilities. The loop still just counts; a lost frame must not abort it.
  • tests/fake_can.py — FakeLiteGripCAN mirrors the new attribute and gains a send_error knob, so a test can make a send fail with a reason.
  • Both READMEs document last_send_error and the typed report.
  • Tests: tests/test_send_failure_reports_its_cause.py (13 cases, all three layers) and one in tests/test_teleop.py.

Testing

From /home/qaz/litegrip-python:

$ python3 -m unittest discover -s tests -t tests
Ran 454 tests in 9.783s

OK (skipped=12)

Baseline on the same machine before this branch: Ran 440 tests ... OK (skipped=12). The 14 added are the 13 in the new module and the 1 in test_teleop.py.

Each new case was checked against the unmodified code — with src/ stashed, all 13 error out; with only teleop.py stashed, the teleop case fails with the old line (the motor is not enabled or the CAN write failed). Nothing is skipped or weakened.

Markdown lint runs clean with the CI version:

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

Issues

Closes #65

`GripperActions._emit()` folded every failed `send_mit_frame()` into
`CommandError("MIT 帧下发失败(未连接或未使能)")`. On the bench the CAN
interface went ENETDOWN mid-move, so that message named a cause nobody had
checked: `is_connected` and `is_enabled` were both True at the instant it was
raised, and the actual `OSError(100, 'Network is down')` existed only as a
`log.warning` attached to no exception. The error table in README.md files a
mid-session write failure under `CommError` and not connected / not enabled
under `NotInitializedError`; neither is a `CommandError`.

The reason now travels. `LiteGripCAN.control_mit()` and
`control_mit_stream()` keep it in `last_send_error`, cleared on success.
`LiteGrip.send_mit_frame()` passes it up and supplies its own
`NotInitializedError` naming the step that was skipped. `_emit()` raises the
type the table names, and when nothing recorded a reason — a coarse
`send_mit_frame` stub, say — it says "原因未记录" rather than guessing. The
trajectory replay abort and the teleop loop's first-failure warning name the
same reason; neither changes what it does about it.

Note for callers: catching `CommandError` around a motion call no longer
catches a failed send. A caller that must keep catching one should catch
`LiteGripError`, the base both types share.

Closes #65
@cao-xiao-hao
cao-xiao-hao merged commit ffc1ee5 into main Oct 14, 2026
1 check passed
@cao-xiao-hao
cao-xiao-hao deleted the fix/a-failed-send-names-its-cause branch October 14, 2026 06:40
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released Shipped in a release label Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Shipped in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report a failed CAN write as CommError, not CommandError

1 participant