Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
90ea9f1
docs: design for opt-in 1-DoF gripper jaw experiment
HipsterBrown Aug 24, 2026
3364ad1
docs: correct jaw-DoF spec after review
HipsterBrown Aug 24, 2026
834ca18
docs: implementation plan for the articulated gripper jaw
HipsterBrown Aug 24, 2026
80b84d7
docs: fix plan issues found in review
HipsterBrown Aug 24, 2026
8399ef8
docs: correct plan's obstructed step-count reference
HipsterBrown Aug 24, 2026
7eeee51
refactor(geometry): share the jaw percent<->angle bijection
HipsterBrown Aug 24, 2026
7ebdd1e
refactor(geometry): name the jaw bijection's unit
HipsterBrown Aug 24, 2026
9e3fe0a
test(geometry): pin saturation, non-zero range, and open-pose magnitude
HipsterBrown Aug 24, 2026
0331eac
refactor(geometry): thread jawDoF through BuildGripperModel
HipsterBrown Aug 24, 2026
97208e3
docs: record that Task 2's guard test is scaffolding
HipsterBrown Aug 24, 2026
a780d52
fixup(geometry): drop redundant test, fix stale BuildGripperModel doc
HipsterBrown Aug 24, 2026
2a2668a
feat(geometry): optional 1-DoF revolute jaw in the gripper model
HipsterBrown Aug 24, 2026
bc4a411
refactor(geometry): tighten gripper jaw-DoF review findings
HipsterBrown Aug 24, 2026
04fc2be
test(geometry): pin jaw pose equivalence between model and meshes
HipsterBrown Aug 24, 2026
5195d29
test(geometry): guard the jaw joint across the module boundary
HipsterBrown Aug 24, 2026
3713e8a
docs: note the 1-ULP bounds hazard for Task 8's GoToInputs validation
HipsterBrown Aug 24, 2026
12a1d95
feat(simulated): add the articulated_jaw config flag
HipsterBrown Aug 24, 2026
1bd89f5
refactor(simulated): split gripper moveTo into setTarget and awaitArr…
HipsterBrown Aug 24, 2026
d6974a8
feat(simulated): wire gripper CurrentInputs and GoToInputs to the jaw
HipsterBrown Aug 24, 2026
b958614
test(geometry): measure planner jaw travel across seeds
HipsterBrown Aug 24, 2026
063b457
fix(simulated): make gripper Stop() actually abort a GoToInputs batch
HipsterBrown Aug 24, 2026
aae3255
feat(simulated): log and expose planner-issued jaw trajectories
HipsterBrown Aug 24, 2026
ad97fcc
test(simulated): document the execute-epsilon hazard for a moving jaw
HipsterBrown Aug 24, 2026
b4449fc
test(simulated): stop the jaw-trajectory test driving full sweeps
HipsterBrown Aug 24, 2026
a8f5aa6
docs: document articulated_jaw and the planner-sensitivity findings
HipsterBrown Aug 24, 2026
6b2b668
fix(geometry): ship the jaw mount orientation as euler_angles
HipsterBrown Aug 24, 2026
881e211
docs: correct the false claim that Geometries() feeds the 3D viewer
HipsterBrown Aug 24, 2026
6fe2009
tools: add jaw.sh for driving the simulated gripper jaw live
HipsterBrown Aug 24, 2026
757cf79
docs: record that the viewer does animate a jointed gripper
HipsterBrown Aug 24, 2026
c57d11d
docs: design for opt-in articulated jaw on the hardware gripper
HipsterBrown Aug 25, 2026
529f1fa
docs: fix three design flaws in the hardware jaw spec
HipsterBrown Aug 25, 2026
cf6d828
docs: third pass on the hardware jaw spec
HipsterBrown Aug 25, 2026
7d2ae64
docs: correct the execute-epsilon hazard's scope
HipsterBrown Aug 25, 2026
255d918
test(simulated): scope the epsilon test's comment to the path it appl…
HipsterBrown Aug 25, 2026
7db30e5
docs: final spec fixes -- risks row, third harness seam, deadlock tes…
HipsterBrown Aug 25, 2026
9d1d7b5
test(geometry): tag the planner jaw measurement behind //go:build nlopt
HipsterBrown Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ calibration wizard. It is bundled into `module.tar.gz` and needs **Node ≥ 20**
## Tests & conventions

- Build/test with `go test ./...` (the Makefile `test` target).
- **`internal/geometry/planner_jaw_test.go` is behind `//go:build nlopt`** and is NOT in the
default build. It is the only file importing `motionplan/armplanning`, which links
`github.com/go-nlopt/nlopt` — a cgo package needing the nlopt C library that CI's runner
does not have, and nothing else here requires. Run it with
`go test -tags nlopt ./internal/geometry`; it takes ~35s.
- Lint/format with `gofmt -s -w .` (the Makefile `lint` target); also run `go vet`.
- Tests that need `VIAM_MODULE_ROOT` must use `testfake.RepoRoot()`, never `"."` — tests run
from their own package directory, not the repo root.
Expand Down Expand Up @@ -226,11 +231,21 @@ calibration wizard. It is bundled into `module.tar.gz` and needs **Node ≥ 20**
cloud assumes). Measured off the follower meshes with the jaws closed;
`TestGripperTCPLiesBetweenTheJawTips` re-derives the jaw bounds from the meshes so
regenerating them can't strand the TCP inside a finger. The `leader` gripper has no jaws, so
its leaf offset is zero. The model is **0-DoF on purpose**: a jaw DoF would become a variable
its leaf offset is zero. The model is **0-DoF by default**: a jaw DoF would become a variable
the motion planner may drive, and it means the frame-system collision meshes are frozen
closed while `Geometries()` keeps serving the live articulating jaw to the 3D viewer.
closed. **`Geometries()` does NOT feed the 3D viewer** — an earlier version of this note claimed
it did, and that claim is false. Verified on a live machine: `GetGeometries` correctly swings the
jaw the full 110° between `Grab` and `Open`, while the viewer does not move at all. The viewer
renders a gripper from `Kinematics()` (`robot/impl/local_robot.go:1362` takes the kinematics
branch and `continue`s, never calling `Geometries()`), so a 0-DoF model renders frozen no matter
what `Geometries()` reports. The viewer *does* poll `GetCurrentInputs` and animate a jointed
gripper — confirmed on a live machine — so `articulated_jaw` animates on Open/Grab while the
default 0-DoF model never will.
`TestGripperFrameResolvesToTCPInFrameSystem` asserts the end-to-end contract, which is also
why users must **not** add a compensating `translation` to the gripper's `frame`.
why users must **not** add a compensating `translation` to the gripper's `frame`. The simulated
gripper's `articulated_jaw` config attribute opts into the 1-DoF version to measure exactly
that risk (the hardware gripper is unchanged, still always 0-DoF); see the sensitivity gotcha
below for what the planner actually did with the freedom.
- **A part's `frame` config and its `kinematics` are two independent things**, and the 3D viewer
reads frames from the kinematics. Verified against a live machine:
`RobotService.FrameSystemConfig` reports `follower-gripper`'s `frame.poseInObserverFrame` as
Expand Down Expand Up @@ -278,6 +293,11 @@ calibration wizard. It is bundled into `module.tar.gz` and needs **Node ≥ 20**
polling a position read failed outright (nothing retries), the same transient that would
abort a move from `WaitForServosToStop`'s own poll -- another reason not to poll `IsMoving`
in a tight loop.
- **`defaultExecuteEpsilon` is inert on the normal `Move()` path.** `services/motion/builtin`'s
`Move` passes `math.MaxFloat64` as the execute epsilon (`builtin.go:260`); the `0.01` default
(`builtin.go:77`) applies only when a caller supplies `executeCheckStart` via the plan/execute
DoCommand split. So the "first trajectory step must match CurrentInputs" check — and the
mid-travel-jaw hazard it creates — does not abort an ordinary arm move.
- **`Speed: 0` means MAXIMUM, not stopped**, and **`Acc: 0` means UNLIMITED, not zero** — both
Feetech register sentinels are the opposite of what they look like, and both have already
caused real bugs in this project. A short-travel joint scaled down by a small `k` can round
Expand Down Expand Up @@ -439,3 +459,31 @@ calibration wizard. It is bundled into `module.tar.gz` and needs **Node ≥ 20**
be mistaken for having done anything. `abort`'s advertised states are unchanged — only the
guard was added. One gap remains by choice: `error` still accepts `abort` without advertising
it, which is harmless because `reset` is the escape hatch offered there.
- **SVA `JointConfig.Min`/`Max` are DEGREES**, while `internal/geometry`'s `GripperJointMin`/`Max`
and every `referenceframe.Limit` are radians. `referenceframe/frame_json.go:152` applies
`DegToRad` for revolute joints (prismatic joints pass through unconverted). Assert *parsed*
`DoF()` limits, never the written config values.
- **A joint that does not affect the goal gets the planner's FULL search range, not none.**
`computeJointSensitivities` (`motionplan/armplanning/linearized_frame_system.go:80`) computes
`startDistance / |myDistance - startDistance|` — *inverse* sensitivity, so a zero-effect joint
divides by zero, yields `+Inf`, and `clampSensitivities` clamps it to `1.0`. This is deliberate
in rdk. **But full search freedom does not become full travel:** measured over 10 seeds, an
unconstrained gripper jaw (`articulated_jaw` on the simulated gripper) moved 0% on unobstructed
plans and only 4%-20% of its range under obstruction, usually returning near its start
(`TestPlannerJawTravel`). Do not assume an unconstrained joint will swing to its limits — and do
not assume it will stay put either.
- **A gripper's `CurrentInputs`/`GoToInputs` are JOINT RADIANS, not a normalized position.**
`[-0.174533, 1.74533]` (-10deg..100deg) for the SO-101 jaw — the frame system requires the same
units and range as the model's `DoF()` limits. There is no 0-1 or 0-100 form on that API; the
percentage lives on the `get_position`/`set_position` DoCommands, and
`geometry.JawRadiansFromPct`/`JawPctFromRadians` convert. Note the gripper API ships raw float64
with **no** degree conversion, unlike the arm API's `Frame.ProtobufFromInput` round trip.
- **The 3D viewer only renders `euler_angles` link orientations correctly.** `spatialmath.Compose`
returns a `*Quaternion`, so `NewOrientationConfig` emits `"type":"quaternion"` — which rdk parses
identically to euler, but which the viewer rendered wrong (the gripper's jaw branch splayed off
at a bad angle). Every orientation in `so101.json` is `euler_angles`. Convert with
`.Orientation().EulerAngles()` before encoding; `TestJawMountOrientationIsEuler` guards it.
- **rdk rejects a joint input that exceeds a limit by even one ULP** (`input out of bounds`). A
sweep computing `min + (range*i)/steps` overshoots by 2.22e-16 at `i == steps`, because `*` and
`/` are left-associative. `GoToInputs` validates against a deliberately wide `jawLimitEpsilon`
for this reason.
5 changes: 2 additions & 3 deletions components/gripper/gripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func newSO101Gripper(ctx context.Context, deps resource.Dependencies, conf resou
meshDetail = geometry.LowDetail
}

model, err := geometry.BuildGripperModel(gripperType, meshDetail, conf.ResourceName().ShortName())
model, err := geometry.BuildGripperModel(gripperType, meshDetail, conf.ResourceName().ShortName(), false)
if err != nil {
return nil, fmt.Errorf("failed to build gripper kinematic model: %w", err)
}
Expand Down Expand Up @@ -335,8 +335,7 @@ func (g *so101Gripper) jawAngle(ctx context.Context) float64 {
if err != nil {
return geometry.GripperJointMin
}
pct := math.Max(0, math.Min(1, percent/100.0))
return geometry.GripperJointMin + pct*(geometry.GripperJointMax-geometry.GripperJointMin)
return geometry.JawRadiansFromPct(percent)
}

// Geometries serves the gripper as meshes: a static body and a moving part posed by
Expand Down
Loading
Loading