Skip to content

feat(primitive): add message and service definitions for gripper capabilities - #289

Merged
HeartLinked merged 4 commits into
syswonder:devfrom
Ciliphen:feat/primitive-gripper
Nov 3, 2026
Merged

HeartLinked merged 4 commits into
syswonder:devfrom
Ciliphen:feat/primitive-gripper

Conversation

@Ciliphen

Copy link
Copy Markdown
Member

Gripper Primitive Interface

Contract overview

Contract ID Mode IDL
robonix/primitive/gripper/info rpc gripper/srv/GetGripperInfo.srv
robonix/primitive/gripper/move rpc gripper/srv/Move.srv
robonix/primitive/gripper/grip rpc gripper/srv/Grip.srv
robonix/primitive/gripper/open rpc gripper/srv/Open.srv
robonix/primitive/gripper/state topic_out gripper/msg/GripperState.msg

info: gripper/GetGripperInfo.srv

Request

No fields.

Response

Field Type Unit Description
ok bool — Query succeeded; metadata valid, not motion readiness
message string — Reason when ok=false; metadata is meaningless in that case
grip_effort_configurable bool — grip accepts a non-default relative_effort
move_speed_configurable bool — move accepts a non-default relative_speed
grip_speed_configurable bool — grip accepts a non-default relative_speed
open_speed_configurable bool — open accepts a non-default relative_speed
opening_width_range_valid bool — When false, ignore the two width bounds below
min_opening_width_m float64 m Minimum permitted clear width between the gripping faces
max_opening_width_m float64 m Maximum permitted clear width between the gripping faces
gripping_force_range_valid bool — When false, ignore the two force bounds below
min_gripping_force_n float64 N Minimum achievable normal grip force per gripping face
max_gripping_force_n float64 N Maximum achievable normal grip force per gripping face

A valid range is finite with 0 <= min <= max. Width bounds apply to the current fingertips and configuration, not to nominal mechanical limits; force bounds are hardware capabilities, not the value corresponding to relative_effort. A *_configurable flag is also false when the corresponding operation is absent.

move: gripper/Move.srv

Request

Field Type Unit Description
opening_width_m float64 m Target clear width between the gripping faces
relative_speed float64 — 0 = configured default; (0,1] = device-relative speed

Response

Field Type Unit Description
ok bool — Accepted and dispatched; arrival not guaranteed
message string — Reason for rejection or dispatch failure

Reject non-finite, negative, or known-unreachable widths; do not silently clamp. The provider maps width to device coordinates, and the mapping need not be linear.

grip: gripper/Grip.srv

Request

Field Type Unit Description
relative_effort float64 — 0 = configured default; (0,1] = device-relative effort. Not newtons, and not a safety force limit
relative_speed float64 — 0 = configured default; (0,1] = device-relative speed

Response

Field Type Unit Description
ok bool — Accepted and dispatched; not proof of contact
message string — Reason for rejection or dispatch failure

Closes adaptively, with no target opening width.

open: gripper/Open.srv

Request

Field Type Unit Description
relative_speed float64 — 0 = configured default; (0,1] = device-relative speed

Response

Field Type Unit Description
ok bool — Accepted and dispatched; not proof that the jaws opened
message string — Reason for rejection or dispatch failure

Drives to the configured open endpoint. It is not a stop, torque-off, sensor-zeroing, or calibration command.

state: gripper/GripperState.msg (topic_out)

Field Type Unit Description
opening_width_valid bool — When false, ignore opening_width_m
opening_width_m float64 m Measured clear width between the gripping faces; finite and nonnegative when valid
grasp_state uint8 — GraspState constant; UNKNOWN marks the observation as untrustworthy

The three fields are independent observations and are not guaranteed to be sampled at the same instant.

GraspState constants (gripper/GraspState.msg)

Name Value Description
UNKNOWN 0 No trustworthy contact observation
NOT_HOLDING 1 Device confirms that no grip is established
HOLDING 2 Contact established; gripping force not guaranteed
SLIPPING 3 Device reports that the established contact is slipping

@Ciliphen
Ciliphen requested a review from enkerewpo as a code owner October 21, 2026 12:55
Copilot AI balanced review requested due to automatic review settings October 21, 2026 12:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@github-actions github-actions Bot added type:feature New feature (feat:) comp:capabilities capabilities/ contracts labels Oct 21, 2026
@enkerewpo

enkerewpo commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Thanks, the contract semantics are clear (dispatch vs. completion, observing state, explicit validity flags), and making the gripper its own abstraction is the right call. A few suggestions:

  1. Add a stop contract. open is explicitly not a stop, so there is currently no way to halt the jaws mid-motion, e.g. when closing on the wrong object.
  2. Consider a timestamp in GripperState, as RGBD and the audio messages have. Grip/contact changes are the moments a verifier or a skill wants to line up with camera frames, and that needs the time of the reading.
  3. The arm contracts still say the gripper is commanded as an extra named joint (arm/joint_command, with its opening in arm/joint_states, see the note in arm/pos_command). That only expresses a target opening; it cannot express an adaptive grip with a force setting or report contact and slip, which is what this PR adds. Vendor stacks separate them the same way even when the gripper shares the arm's controller or bus: libfranka has a separate Gripper with grasp(width, speed, force), and ros2_control drives grippers with a gripper controller rather than the arm's joint trajectory controller. Please update the arm contracts here or in a follow-up so gripper commands only go through gripper/*. The gripper's opening may still appear in the arm's joint states where a robot model needs it for TF. For an arm with a built-in gripper, one primitive package can provide both arm/* and gripper/* over the same SDK connection. You don't need to stay compatible with the current arm abstraction; it is due for a redesign.

References

How common arm stacks expose the gripper (separate commands even when it shares the arm's controller, bus or SDK):

  • libfranka: a separate franka::Gripper class with move(width, speed), grasp(width, speed, force, ...) and stop(), alongside franka::Robot for the arm. Gripper class reference, gripper.h
  • AgileX Piper SDK: the arm and gripper share one CAN interface, but the gripper has its own GripperCtrl call, separate from the six-joint JointCtrl. Interface reference
  • UFactory xArm Python SDK: set_gripper_position (and related gripper calls) next to the arm's set_servo_angle. xArm API
  • ros2_control: grippers are driven by a dedicated parallel_gripper_action_controller with a ParallelGripperCommand action, not by the arm's joint trajectory controller. Parallel gripper controller docs
  • MoveIt: the gripper is configured as its own planning group and end effector in the Setup Assistant, separate from the arm group. MoveIt Setup Assistant tutorial

Grippers need stop control and contact-aware commands separate from arm
joint positioning. Make supported grip request forms explicit so
providers can accept a target opening width, adaptive closing, or both.

Add a stop contract and a standard Time stamp for newly acquired state
observations. Define the open endpoint through known width limits and
centralize relative-setting validation and per-command defaults.

Validation passed for protobuf, ROS 2 and MCP interface generation,
protobuf compilation, all 47 generated MCP modules, and the contract/IDL
comment consistency unittest. A two-process gRPC smoke test passed for
info, optional-width grip, stop and timestamped state. No physical
device validation or colcon build was performed.

BREAKING CHANGE: Route gripper commands through gripper/*, not
arm/joint_command. Regenerate gripper bindings for the updated request
and state schemas.

Assisted-by: Claude Code:gpt-6.1-sol
The grip width is an expected object size, not a jaw-position target.
Rename the request fields and matching info flags to make that meaning
explicit, and document both states of the presence flag.

Keep positioning and measured-opening field names unchanged. A width
match does not establish contact or task success; providers use their
configured gripping tolerances.

Assisted-by: Claude Code:gpt-6.1-sol
@Ciliphen

Copy link
Copy Markdown
Member Author

Thanks for the review. I've updated the contracts:

  • Added gripper/stop. The contract requires it to remain dispatchable during motion. Its response only acknowledges acceptance and dispatch; it does not guarantee physical standstill, continued holding force, or safety-rated emergency stopping.
  • GripperState now uses one builtin_interfaces/Time stamp, defined as the acquisition-round start in Unix epoch time, with (0, 0) meaning unknown. Only newly acquired observations are valid; cached, failed, or unsupported readings must be marked invalid. This does not imply atomic hardware sampling.
  • Removed gripper actuation from the arm contracts. One primitive package can still provide both arm/* and gripper/* over a shared SDK connection.
  • grip now accepts an optional expected object width through expected_object_width_valid and expected_object_width_m. This is a size criterion evaluated with provider-configured tolerances, not a jaw-position target. A width match alone does not prove contact or task success. info separately advertises support for requests with and without this criterion.

I also centralized the shared relative-parameter rules in GetGripperInfo.srv and clarified that open targets max_opening_width_m when the reported range is valid. Arrival can only be inferred when both the target and valid width feedback are available.

@HeartLinked HeartLinked left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm writing a provider for a Jodell RG75-300 against these contracts. It is a servo parallel gripper on a separate RS-485 bus (Modbus RTU through a USB adapter on the host), next to an AUBO arm, so it has its own package and does not share the arm SDK. Everything maps cleanly except two points below.

  1. GripperState cannot tell a finished empty grip from one still closing.

After grip without an expected width, the caller waits for the jaws to stop. On an empty grip grasp_state is NOT_HOLDING the whole time, so the only way to see the end is to watch opening_width_m stop changing, which is the inference GraspState.msg rules out for contact. The RG75 reports this directly: its status register has a go-to bit (gGTO, 1 while moving to the target), and its object-detection field reads 0 while the fingers are still moving. control_msgs/action/ParallelGripperCommand carries stalled and reached_goal for the same reason.

Suggestion: add to GripperState a device-reported motion state with the same validity rule as grasp_state, e.g. uint8 motion_state with UNKNOWN=0, MOVING=1, STOPPED=2. A provider without such feedback reports UNKNOWN.

  1. Soma and the docs still read the gripper from arm/joint_states.

This PR removes the gripper from the arm contract descriptions, but Soma's gripper state still comes only from that stream (system/soma/src/store.rs collect_grippers, system/soma/src/runtime_monitor.rs JOINT_STATES), and so do system/soma/README.md, interface-catalog/primitive/arm.md, the gripper state section of integration-guide/vendor-onboarding.md and tutorials/existing-python-feature.md in robonix-book. A gripper on its own package, like this one, will never reach Soma. Could the PR description say whether Soma moves to gripper/state in a follow-up, and whether the opening may still appear in the arm's joint_states for TF as suggested in the review? The new joint_states description ("arm joint positions") reads as if it may not. An interface-catalog page for gripper/* would also help.

For reference, the RG75 mapping I plan to use: grip writes position 0xFF with force and speed scaled from relative_effort and relative_speed; object-detection 2 (stopped on an object while closing) or 1 (while opening) maps to HOLDING, 3 (reached the target with no object) to NOT_HOLDING, and 0 or a failed read to UNKNOWN. The manual warns that very thin objects may not register, so NOT_HOLDING from this device is not proof the jaws are empty. The device's own activation sweep (a full open/close at power-up) goes in the driver's activate step, not in a contract. I'll report back after it runs on the hardware.

Contact and width observations cannot reliably distinguish moving jaws
from a stopped empty grip. Add an independent device-reported motion
observation with UNKNOWN, MOVING and STOPPED values.

Append motion_state without changing existing protobuf field numbers.
Keep it separate from contact and command outcomes, use UNKNOWN for
unavailable readings, and retain the shared acquisition timestamp.

Protobuf, ROS 2 and MCP generation, protobuf/MCP compilation and the
contract/IDL consistency test passed. A two-process gRPC test exercised
motion/contact combinations. Unknown defaults and old/new protobuf
payload checks passed. ROS 2 consumers need regenerated interfaces.
No physical-device validation or colcon build was performed.

Assisted-by: Claude Code:gpt-6.1-sol
@Ciliphen

Ciliphen commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Thanks for checking the contracts against a standalone gripper provider. I've added device-reported motion_state to GripperState, with UNKNOWN=0, MOVING=1, and STOPPED=2. Unsupported, failed, or cached observations must report UNKNOWN.

Motion and contact remain independent. STOPPED does not establish that a particular command completed successfully, that the requested width was reached, or that an object is held. It supplies the missing observation without turning GripperState into a command-result protocol.

You're also right about Soma, the modification of Soma and the docs documents has not been carried out yet.

@HeartLinked HeartLinked left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for adding the motion state. I'll open a follow-up PR for point 2: moving Soma's gripper state to gripper/state and updating the docs.

@HeartLinked
HeartLinked merged commit 76cfca2 into syswonder:dev Nov 3, 2026
14 checks passed
@Ciliphen
Ciliphen deleted the feat/primitive-gripper branch November 3, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:capabilities capabilities/ contracts type:feature New feature (feat:)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants