Skip to content

feat: add leader/follower gripper teleoperation - #11

Merged
yd-sl merged 1 commit into
mainfrom
feat/gripper-teleop
Sep 29, 2026
Merged

yd-sl merged 1 commit into
mainfrom
feat/gripper-teleop

Conversation

@yd-sl

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

Copy link
Copy Markdown
Contributor

Summary

Adds leader/follower teleoperation: one gripper mirrors another. The leader goes slack so its jaws can be pushed by hand and publishes how far open it is; the follower drives its own jaws to match. What travels over the wire is a normalized opening in [0, 1], not an angle, so the two ends need not share a calibration, mount, or zero point.

The algorithm is ported from litearm_device.gripper_teleop, with the hard zenoh dependency replaced by a pluggable TeleopTransport so the SDK stays standard-library-only.

Changes

  • src/litegrip/teleop.py (new): GripperTeleop (master/slave loops, watchdog, align), frame codec (>4d, 32 bytes, byte-compatible with litearm), openness <-> rad conversion, and two transports — UdpTeleopTransport (default) and InProcTeleopTransport.
  • src/litegrip/gripper.py: teleop_start / teleop_stop / teleop_status; a reentrant _io_lock around send_mit_frame / poll / get_state so the teleop thread and the caller can share one CAN bus; disconnect() stops an active session first.
  • src/litegrip/__init__.py: exports the teleop classes, codec helpers, and FRAME_SIZE.
  • tests/test_teleop.py (new): codec round-trip, conversion for both mounts, in-proc and UDP transports, master/slave loops, watchdog hold, and the LiteGrip API surface.
  • examples/teleop.py (new): runnable one-end-per-machine runner (--mode master|slave).
  • README.md / readme_zn.md: a teleoperation section.

Two deliberate divergences from the litearm original:

  • The openness -> rad conversion carries close_sign, which the original omitted, so a reverse-mounted follower moves the correct way.
  • A follower that loses its leader holds its position at the follow gains instead of relaxing to zero torque. This means it keeps pressing whatever is between the jaws, matching the original's behavior; the README states this explicitly.

UdpTeleopTransport is plain, unauthenticated UDP and is documented as trusted-network-only.

Testing

python3 -m unittest discover -s tests -t tests -v

105 tests, all pass. Also verified a single-process master -> slave loopback with two fake-CAN grippers: the reverse-mounted follower tracks openness=1 to its own open limit (direction not inverted), and teleop_status() reports frames / stale / openness as expected.

npx --yes markdownlint-cli2@0.23.3 "README.md" "readme_zn.md"

0 issues.

Real-hardware validation (two grippers, one per machine) has NOT been run yet — it needs the operator present.

Issues

None.

Let one gripper mirror another: the leader goes slack so its jaws can be
pushed by hand and publishes how far open it is, the follower drives its own
jaws to match. The wire carries a normalized opening in [0, 1], not an angle,
so the two ends need not share a calibration, mount, or zero point.

The algorithm is ported from litearm_device.gripper_teleop, but the transport
is a pluggable TeleopTransport instead of a hard zenoh dependency, so the SDK
stays stdlib-only: UdpTeleopTransport for real links and InProcTeleopTransport
for tests and two grippers in one process.

The openness<->radian conversion carries close_sign, which the litearm original
omitted, so a reverse-mounted follower moves the correct way. A follower that
loses its leader holds its position at the follow gains rather than relaxing.
@yd-sl
yd-sl merged commit a6c03af into main Sep 29, 2026
1 check passed
@yd-sl
yd-sl deleted the feat/gripper-teleop branch September 29, 2026 02:42
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.5.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

yd-sl added a commit that referenced this pull request Sep 29, 2026
* feat: run gripper teleop over the validated zenoh link

The teleop shipped in #11 invented its own network layer -- a plain-UDP
transport on `litegrip/teleop/{master_id}` -- and was merged on hardware-free
tests alone. The system actually validated on hardware is a zenoh
point-to-point link, so the SDK now uses that structure: the topic and port
come from the shared litearm namespace, both ends run in peer mode with
multicast and gossip off, the leader listens on a TCP port and the follower
connects to it, and the frame stays byte-identical to the litearm stack's.

zenoh is an optional extra (`pip install litegrip[zenoh]`), resolved lazily,
so `import litegrip` still works on a machine that will never teleoperate.
The UDP transport stays available through `link="udp"` and is no longer the
default.

Alongside the transport, port the safety rules the validated design spec has
and #11 was missing: non-finite frames are dropped at the wire boundary
rather than clamped onto a hard stop; the follower's target is clamped into
its own calibrated travel every cycle; a `send_mit_frame` that returns False
and a gripper `error_code` other than "enabled" are counted instead of
swallowed; follow gains fall back to the calibration's own kp/kd; a
non-positive watchdog is rejected at construction; staleness is read from the
slot's "never received" state rather than a 0.0 timestamp sentinel; teleop
refuses to start on an uncalibrated or zero-travel gripper; and the leader's
zenoh listener is resident for the life of the gripper, because rebuilding it
per session leaves the port bound and makes matching fail intermittently.

* fix: import teleop_topic outside the zenoh guard in the tests

`litegrip.teleop` has no zenoh dependency, so the import belongs at module
level. Behind the `HAVE_ZENOH` guard it left `teleop_topic` undefined when
zenoh is absent, and the module failed to import instead of skipping — which
is exactly the machine CI runs on.
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