Skip to content

feat: report a stale status read, and let the caller refresh it - #68

Merged
cao-xiao-hao merged 1 commit into
mainfrom
feat/report-a-stale-status-read
Oct 14, 2026
Merged

cao-xiao-hao merged 1 commit into
mainfrom
feat/report-a-stale-status-read

Conversation

@cao-xiao-hao

Copy link
Copy Markdown
Contributor

Summary

A status read could not tell a measurement from a memory. The readings get_state() returns are a cache, and only a status frame updates it. A DM motor answers the control frames it receives, so a gripper that is being driven reports on its own — but one holding a pose, or sitting disabled, sends nothing, and get_state(wait=True) then waited out its 50 ms and handed back the previous frame with no sign that nothing had answered. This makes the snapshot say which happened, and adds the request that gives an idle motor something to answer.

Breaks a public field's meaning for consumers; publishes as a minor under this policy. GripperState.timestamp used to be the time of the call that returned the snapshot and is now the arrival time of the frame behind it, 0.0 when no frame has ever arrived. Code that timed a loop with it now reads a stale constant, and code that read it as a heartbeat should read fresh instead. A major was not approved for this, so the marker is deliberately absent — if you want it, the newest tag is v1.0.2, and the transition would be v1.0.2 -> v2.0.0.

Changes

  • GripperState gains fresh (a frame answered this read), data_age_s (seconds since the frame behind the snapshot arrived, None when there never was one), and is_stale(stale_after_s=STALE_AFTER_S); STALE_AFTER_S = 0.5 s is exported from litegrip, the threshold the C++ SDK uses.
  • LiteGrip.refresh_status() and LiteGripCAN.refresh_status() send the 0xCC status request, which makes an idle motor speak without moving the jaws. It reports like enable(): False with a warning when the request did not go out.
  • LiteGripCAN gains last_update (monotonic) and last_frame_time (wall clock); MotorState records the second beside the first, because the receive path stamps frames with time.monotonic() and an age has to be measured on that clock.
  • get_state() passes the frame's stamps through instead of stamping the call, and returns fresh=bool(answered).
  • README.md and readme_zn.md: the corrected get_state() row, a refresh_status() row, and a section on what fresh, data_age_s, timestamp and is_stale() mean.
  • tests/test_state_staleness.py: 12 tests over an idle bus, a never-speaking motor, the request path, and the failed write.
  • tests/fake_can.py: FakeLiteGripCAN can go idle (delivers), serve a request (refresh_ok, refresh_pending, refresh_calls), and carries the two stamps.

Testing

$ python3 -m unittest discover -s tests -t tests
Ran 452 tests in 9.851s

OK (skipped=12)

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

The new tests are not vacuous: forcing timestamp=time.time(), fresh=True, data_age_s=None in get_state() fails 5 of the 12.

Issues

Closes #66

A status read could not tell a measurement from a memory.  The readings
get_state() returns are a cache, and only a status frame updates it.  A
DM motor answers the control frames it receives, so a gripper that is
being driven reports on its own — but one holding a pose, or sitting
disabled, sends nothing, and get_state(wait=True) then waited out its 50
ms and handed back the previous frame with no sign that nothing had
answered.

Say which happened, and give the caller a way out:

- GripperState gains fresh (a frame answered this read), data_age_s
  (seconds since the frame behind the snapshot arrived, None when there
  never was one), and is_stale(stale_after_s=STALE_AFTER_S), plus the
  module-level STALE_AFTER_S = 0.5 s — the threshold the C++ SDK uses,
  so a guard reads the same on either side.
- LiteGrip.refresh_status() and LiteGripCAN.refresh_status() send the
  0xCC status request, which makes an idle motor speak without moving
  the jaws.
- MotorState records last_frame_time (wall clock) beside last_update
  (monotonic).  The receive path stamps frames with time.monotonic(), so
  an age has to be measured on that clock; a stamp for a human to read
  needs one of its own.

Note for callers: GripperState.timestamp used to be the time of the call
that returned the snapshot, and is now the arrival time of the frame
behind it — 0.0 when no frame has ever arrived.  Code that timed a loop
with it reads a stale constant instead of the current time, and code
that read it as a heartbeat should read fresh instead.  That is a public
field changing meaning; under the release rules this publishes as a
minor.

Closes #66
@cao-xiao-hao
cao-xiao-hao force-pushed the feat/report-a-stale-status-read branch from 7f7499f to 4f49b93 Compare October 14, 2026 06:42
@cao-xiao-hao
cao-xiao-hao merged commit cacf392 into main Oct 14, 2026
1 check passed
@cao-xiao-hao
cao-xiao-hao deleted the feat/report-a-stale-status-read branch October 14, 2026 06:44
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.1.0 🎉

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 stale read from get_state() instead of returning it as fresh

1 participant