feat: add gripper trajectory record and replay - #18
Merged
Merged
Conversation
The SDK could move the jaws and mirror one gripper onto another, but it could not teach a motion once and repeat it: every approach path, seating wiggle or squeeze profile had to be re-written as code, with no way to capture one off the hardware. Recording puts the motor into zero-gravity so the jaws can be pushed by hand, samples the opening, and can save it to a file. Replay streams the captured openings back as MIT command frames, on a wall clock rather than an index per cycle, so a slow cycle skips ahead instead of making the motion longer than it was taught. What is stored is the opening normalised by the recording unit's travel -- the same channel teleop sends -- so a trajectory taught on a normal-mount gripper replays on a reverse-mounted one. Recorded torque and velocity are kept as diagnostics and never fed forward: replay commands position, not force, so a squeeze that was taught against an object repeats as a position path and not as the same grip force. Recording, replay and teleoperation now claim one session slot under a lock, so two of them cannot interleave frames on one motor; disconnect tears whichever is running down. The hold-position frame the teleop master and a stopping replay both need is one extracted primitive. Traps worked around, following litearm-core's logging rules: a sample whose timestamp did not advance is not stored and a stopped clock aborts either loop; a capture that did not fill raises and reports the count instead of returning a short recording as if it were whole; and a file whose length does not match the sample count in its header is rejected rather than parsed into half a trajectory.
The unit suite covers record and replay case by case, but nothing ran the whole path in one go and printed what happened, so verifying the feature by hand meant reading a test file. tests/trajectory_check.py teaches a path, saves it, loads it back and replays it on the recording unit and on a reverse-mounted one, printing every number behind each step and exiting nonzero if any step fails. It is deliberately not named test_* so unittest discover does not collect it twice. The motion comes from tests/fake_can.py and the clock is fake, so the run takes milliseconds and the assertions are equalities rather than tolerances around a scheduler: the capture matches the hand to 2e-16, and the replay matches the trajectory to 5e-14. It proves nothing about real hardware, which still needs a gripper and examples/trajectory.py.
|
🎉 This PR is included in version 0.9.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The SDK could move the jaws and mirror one gripper onto another, but it could not teach a motion once and repeat it: every approach path, seating wiggle or squeeze profile had to be written as code, with no way to capture one off the hardware.
This adds recording and replay.
record()puts the motor into zero-gravity so the jaws can be pushed by hand, samples the opening, and returns aTrajectorythat can be saved to a file;play()streams the captured openings back as MIT command frames. The reference is litearm-core'slogmodule, and the three rules it enforces are ported rather than documented: never return half data as a success, reject a stream whose length is not an exact multiple of the sample size, and refuse to loop forever on a source that stopped advancing — the last one applied to the sample clock in both directions.What is stored is the opening normalised by the recording unit's travel, the same channel teleop sends, so a trajectory taught on a normal-mount gripper replays on a reverse-mounted one. Recorded torque and velocity are kept as diagnostics and never fed forward: replay commands position, not force, so a squeeze taught against an object repeats as a position path and not as the same grip force.
This is additive. No existing signature, behaviour or default changes, so nothing breaks for consumers and the change publishes as a minor under the repository's version policy.
Rebased onto
mainat 4eeb87d. This supersedes #13, which was opened before #14–#17 landed and could not be merged without rewriting its pushed branch. Three conflicts were resolved by hand:disconnect()— main's new_close_teleop_pub()is kept alongside the trajectory teardown.teleop_start()— main's rewritten zenoh/udp transport selection (link,grip_id,dq_max,check_ready) is kept, now inside the session claim'stry/except. Without that bracket a refused transport (bad link, missing host, port already bound) would leave the session slot claimed for the life of the process, refusing every later teleop, record and replay in turn.__init__.py— both the zenoh link exports and the trajectory exports are kept.Changes
src/litegrip/trajectory.py—TrajectorySample(frozen dataclass:t,openness,position_rad,velocity_rad_s,torque_nm),Trajectory(samples plus provenance,openness_at,to_bytes/from_bytes,save/load),TrajectoryRecorder,TrajectoryPlayer, and the error hierarchy underLiteGripError.LGRTRJ01magic, version, sample count,sample_hz,created, the unit's closed/open limits andrad_to_mm,can_id,mount) followed by 40-byte little-endian samples.from_bytesrejects wrong magic, unknown version, a length that is not exactly66 + n*40(trailing bytes included), non-positivesample_hz, zero travel, non-finite numbers, non-monotonict, and an undecodablemount.(kp, kd) == (0, 0)every cycle in zero-gravity mode so the motor never self-locks its comm-loss fault mid-teach, and leaves zero-gravity in afinallyso a failed capture cannot leave the jaws slack.record_stop()raises with the sample count when the loop died and raisesTrajectoryEmptyErrorwhen nothing landed.elapsed = (now - t0) * speed), so a slow cycle skips ahead instead of stretching the motion;aligndoes onegoto_radto sample 0 first;loopwraps by whole loops so an overrunning cycle keeps its phase.src/litegrip/gripper.py— seven methods onLiteGripin the shape of the teleop block:record,record_start,record_stop,play,play_start,play_stop,trajectory_status. Adds_session_lockwith_claim_session/_release_sessionso recording, replay and teleoperation cannot interleave frames on one motor;disconnect()tears a trajectory session down. Extracts the hold-position frame shared by the teleop master and a stopping replay into_hold_position().src/litegrip/__init__.py— exportsTrajectory,TrajectorySample,TrajectoryRecorder,TrajectoryPlayer,trajectory_dir,resolve_path,DEFAULT_RATE_HZand the five error classes.tests/test_trajectory.py— 74 hardware-free tests againsttests/fake_can.py, with the timing seams stubbed and threads driven by a deterministic fake clock.tests/trajectory_check.py— a readable end-to-end run of the whole path, printing every number it checks and exiting nonzero if any step fails. Deliberately not namedtest_*sounittest discoverdoes not collect it twice.examples/trajectory.py—--record SECONDS --save NAME,--play NAME,--list, sameargparse+--dry-runshape asexamples/teleop.py.README.md/readme_zn.md— a "Trajectory record and replay" / "轨迹录制与回放" section in both languages, with a quick start, the method table, CLI examples, the file and path rules, and the warnings that matter (position not force; jaws are slack duringrecord(); only one hold frame from a blockingplay(); recording, replay and teleop are mutually exclusive; the unit must be calibrated).Testing
Merged state, on the rebased tree:
225 is this branch's 179 plus the 46 added by #14–#17 on
main; the whole suite was re-run after the rebase, not carried over. It is hardware-free —tests/fake_can.pysimulates the motor kinematics and the CAN layer — so green here is the whole check.That script was written for this PR and verified by breaking what it checks: eight defects were injected into
trajectory.py/gripper.pyone at a time (a shifted sampling instant, recorded velocity fed forward asdq, a truncated file accepted, teaching frames carrying gains, the recorded calibration used instead of the local one,speedignored,play_startnot claiming the session, uncalibrated units allowed to record) and each was caught by a named check before the source was restored.CLI, run without hardware under a scratch trajectory directory:
--dry-runproves the CLI builds its plan without a CAN interface; it does not prove motion, which needs a real gripper.Issues
No linked issue —
gh issue list --search "trajectory OR record OR replay" --state allfound none, and this was requested directly.