Skip to content

feat: add mount direction, per-channel calibration and mount templates - #7

Merged
thetooler merged 1 commit into
mainfrom
feat/mount-direction-and-calibration
Oct 6, 2026
Merged

thetooler merged 1 commit into
mainfrom
feat/mount-direction-and-calibration

Conversation

@thetooler

Copy link
Copy Markdown
Contributor

Summary

Stage 1 of bringing this SDK up to the online litegrip-python feature set: mount direction (normal / reverse), per-channel calibration, and mount templates. Ported from the Python SDK's GripperConfig.close_sign / mount, load_template(), default_calib_path() and its per-channel load chain.

What changed

  • Direction comes from the limits. GripperConfig::close_sign() is derived (the numerically larger limit is the closed side) and every mm/force conversion uses it; mount() reads the direction back. No separate field, so it cannot disagree with the limits.
  • calibrated flag. False until a calibration or a template loads. mount() reports nothing while it is false — the placeholder ordering is also close-above-open, so "normal" before a measurement is a claim, not a reading. (Raw goto_rad/move_to stay ungated, the same split as Python where only limit_target/press_target check the flag.)
  • Calibration is per channel: ~/.litegrip/<channel>_calibration.json, then the legacy single-file location, then the factory file. The automatic chain skips candidates declaring a different channel (every gripper ships at CAN id 0x08 — the channel is the only identity key). An explicit path is the caller's override and loads with a warning instead.
  • Mount templates calibration/normal.json / reverse.json plus LiteGrip::load_template(name). Strict by design: an unknown name throws, and an unreadable template throws — never a fallback to the factory file, which is a normal mount. Templates carry no channel, ids or gains (pinned by test).
  • save_calibration() without a path writes the per-channel file, stamps calibrated: true, and omits an unknown mst_id (a falsy 0 would pin the RX filter to 0x000).
  • disable_on_disconnect (default true) mirrors the Python attribute; GripperBus::disconnect(bool disable = true) gains the corresponding parameter.
  • home() targets the configured closed limit rather than the constant, so a reverse mount homes to its own closed end.
  • The three calibrate*() routines mark the config calibrated.

Two things worth a reviewer's attention

  1. Found and fixed a latent bug in the shipped placeholder limits: kPosClosedRad/kPosOpenRad were swapped relative to the Python SDK (0.0 / 1.14 instead of 1.14 / 0.0) and against their own comment, which made default-config mm readings negative. Nothing exercised them because every test and deployment loads a calibration file. Corrected to the Python values.
  2. Normal-mount behaviour is bit-for-bit unchanged — close_sign() is +1 there, making the extra multiply exact; the tests assert equality rather than a tolerance for the mm and force conversions. litegrip_ros2_control / litegrip_moveit_config, which consume position_mm, see no change for the normal mount.

Verification

  • Full suite green: configure + build + ctest --test-dir build --output-on-failure — 12/12, with new coverage for template contents and strictness, the calibrated flag, the channel-skip chain (a can1 unit must adopt neither can0's legacy file nor can0's factory file), per-channel save, reverse mm/force signs, and the bit-identical normal-mount arithmetic.
  • Rerun from a clean /tmp after deleting the fixture directories — the check that caught the /tmp leak in feat: add the LiteGrip C++ SDK #5.
  • markdownlint-cli2@0.23.3 "README.md" locally: 0 issues.

This is stage 1 of the multi-stage parity plan; the motion engine / force control stage follows in a separate PR.

The calibrated limits now carry the mounting direction: whichever is
numerically larger is the closed side, and every mm/force conversion
derives its sign from that ordering (GripperConfig::close_sign()). A
reverse mount has the ordering flipped, so direction stays data rather
than a second field that could disagree with the limits.

- Fix the shipped placeholder limits: kPosClosedRad/kPosOpenRad were
  swapped relative to the Python SDK (0.0/1.14 instead of 1.14/0.0),
  which made the default-config mm readings negative. Nothing exercised
  them because every test and deployment loads a calibration file.
- GripperConfig gains `calibrated` (false until a calibration or a
  template is loaded) and `mount()` -- which reports nothing while
  uncalibrated, because the placeholder ordering is also close-above-
  open and claiming "normal" before any measurement would be a claim,
  not a reading.
- home() targets the configured closed limit instead of the constant,
  so a reverse-mounted unit homes to its own closed end.
- Calibration is per channel: ~/.litegrip/<channel>_calibration.json
  first, then the legacy single-file location, then the factory file.
  The automatic load skips a candidate that declares a different
  channel -- every LiteGrip ships at CAN id 0x08, so the channel is the
  only identity key on a two-gripper machine. An explicit path is the
  caller's override and still loads (with a warning).
- Mount templates (calibration/normal.json, calibration/reverse.json)
  declare a direction by name via load_template(); an unknown name
  throws, and an unreadable template never falls back to the factory
  file -- that fallback is a normal mount, the one silent answer the
  template name exists to prevent. Templates carry no channel, ids or
  gains, so loading one cannot rewrite what the caller set.
- save_calibration() without a path writes the per-channel file (the
  one the chain reads first), stamps `calibrated: true`, and omits an
  unknown mst_id instead of writing the falsy 0 that would pin
  auto-detection to RX filter 0x000.
- disable_on_disconnect (default true): disconnect() can leave the
  motor enabled and holding, mirroring the Python attribute.
- calibrate()/calibrate_guided()/calibrate_manual() mark the config
  calibrated; a measured run is what the flag claims.

Normal-mount behaviour is bit-for-bit unchanged: close_sign() = +1
makes the extra multiply exact, and the tests assert equality rather
than tolerance for the mm and force conversions.

Tests: template contents and strictness, the calibrated flag, the
channel-skip chain (a can1 unit must adopt neither can0's legacy file
nor can0's factory file), per-channel save, reverse mm/force signs and
the bit-identical normal-mount arithmetic.
@thetooler
thetooler merged commit 0f7d811 into main Oct 6, 2026
1 check passed
@thetooler
thetooler deleted the feat/mount-direction-and-calibration branch October 6, 2026 05:39
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 0.2.0 🎉

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