You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The shipped 2F descriptions model the driven joint as the revolute knuckle, so gripper_cmd goals and /joint_states are in radians against gripper_closed_position. That is the wrong parameterization for a parallel gripper: what callers want is the jaw opening, and every consumer that needs it converts radians back to millimetres on its own.
Parallel grippers are normally modelled as a slider, with the linkage as passive joints for visuals. Franka's hand is the reference shape — finger_joint1 is type="prismatic" in metres, finger_joint2 is a <passive_joint> in the SRDF, and its group states are metres. This description is revolute because it was modelled on the visible knuckle rather than on the useful quantity.
Why it matters
The MCP server carries its own radians<->mm map (mcp/gripper_mcp/units.py), whose docstring already calls itself a stopgap. It cannot even own the calibration: robot_description.py reads the command joint and gripper_closed_position out of the driver's URDF at runtime so the two agree on "closed".
The tsf_demo consumers work in mm end to end — grip-width thresholds, opening as a model feature for object recognition — and bypass the ROS driver entirely, opening the serial port with pyRobotiqGripper to call position_mm().
That makes three definitions of one conversion: the driver's, the MCP's, and pyRobotiqGripper's.
Command side. Add a prismatic command joint in metres per model, on a dummy child link, and re-point the existing mimics from the knuckle joint to it. The relation is exactly linear, so a plain <mimic multiplier="..." offset="..."/> covers it — offset is already used in robotiq_2f_140.xacro. Do not chain mimics; resolution is single-level in both robot_state_publisher and ros2_control.
Keep the radians joint for one release, behind an opt-out xacro flag, so consumers migrate on their own schedule. Humble's retirement on 2027-05-31 is a natural sunset. Both command interfaces start NaN and ros2_control claims command interfaces exclusively, so an unclaimed one is never written — "the non-NaN one wins" is a direct read of what the controller manager handed out, not a heuristic. Two claimed at once is a configuration error to report.
Derive, do not duplicate. The metres mapping comes from the SDK (units::openingFromRegister / openingToRegister); the radians one is derived from it, not a second independent mapping off gPO. Otherwise this reintroduces exactly the duplication Consume the SDK's SI conversions #70 exists to remove.
Knock-ons
The driver reads info_.joints.at(0) and is already joint-name agnostic, so the rename costs it nothing. What changes is the scale source: gripper_closed_position (one number) becomes a DeviceProfile. That parameter shape is shared with #44, #45 and #46, so it should be settled once.
Acceptance criterion: all six DeviceProfile fields come from the description, the count band included. The SDK's arithmetic is already parameterized — openingFromRegister / openingToRegister work off profile.registerPositionRange(), so a hardcoded band cannot exist inside the conversions — but profiles::k2F85 hardcodes openPosition 3 and closedPosition 230. A driver that passes the shipped profile has moved the hardcoding to another repo rather than removed it.
Those two counts are installation state, not model constants: non-standard fingertips are common and meet before the linkage runs out of travel, so the closed end lands below 230 (robotiq/grippers#34 has the detail and the bench test that confirms it). Today gripper_scaling.hpp hardcodes 3 and 230 for every model and takes gripper_closed_position as its only position parameter, so an installation whose fingers meet early gets compressed feedback and intermediate commands that overshoot — a half-closed command reaches for count 116 when half the real travel is nearer 101.
The pattern already exists in the driver: gripper_min_speed / gripper_max_speed arrive as <hardware> parameters rather than from k2F85, because the description is the authority on the installation. openPosition / closedPosition should arrive the same way, with the shipped profile as the fallback for anyone who has not measured. DeviceProfile is a plain struct with public members, so the driver can construct one from parameters and the SDK needs no change.
The failure mode is quiet: the band inherits k2F85 while the parameterization is designed around opening, speed and force, and nobody notices until a gripper with custom tips goes to the wrong place.
Numbers that are silently in the wrong unit once the command joint is metres:
Gazebo drops a link with no inertial, so the dummy link needs a token one or an xacro:unless sim_gazebo. The extra joint also appears in the MoveIt Setup Assistant's joint list and in /joint_states; a chain-based group or an SRDF that names joints explicitly will not pick it up, but it is worth a comment in the xacro and a line in the README.
The description tests are already parameterized on (model, joint), so a rename flows through them; test_ros2_control_urdf.py has a mimic-joint test to extend.
Blocks
#70 item 3, and with it the MCP's units.py / robot_description.py.
The shipped 2F descriptions model the driven joint as the revolute knuckle, so
gripper_cmdgoals and/joint_statesare in radians againstgripper_closed_position. That is the wrong parameterization for a parallel gripper: what callers want is the jaw opening, and every consumer that needs it converts radians back to millimetres on its own.Parallel grippers are normally modelled as a slider, with the linkage as passive joints for visuals. Franka's hand is the reference shape —
finger_joint1istype="prismatic"in metres,finger_joint2is a<passive_joint>in the SRDF, and its group states are metres. This description is revolute because it was modelled on the visible knuckle rather than on the useful quantity.Why it matters
mcp/gripper_mcp/units.py), whose docstring already calls itself a stopgap. It cannot even own the calibration:robot_description.pyreads the command joint andgripper_closed_positionout of the driver's URDF at runtime so the two agree on "closed".position_mm().Plan
GripperStatusmessage from Publish object_status (gOBJ) and motor_current so clients can read them #74. This is what the MCP actually needs and it needs no description work at all; it can land first and on its own.<mimic multiplier="..." offset="..."/>covers it —offsetis already used inrobotiq_2f_140.xacro. Do not chain mimics; resolution is single-level in bothrobot_state_publisherandros2_control.ros2_controlclaims command interfaces exclusively, so an unclaimed one is never written — "the non-NaN one wins" is a direct read of what the controller manager handed out, not a heuristic. Two claimed at once is a configuration error to report.units::openingFromRegister/openingToRegister); the radians one is derived from it, not a second independent mapping off gPO. Otherwise this reintroduces exactly the duplication Consume the SDK's SI conversions #70 exists to remove.Knock-ons
The driver reads
info_.joints.at(0)and is already joint-name agnostic, so the rename costs it nothing. What changes is the scale source:gripper_closed_position(one number) becomes aDeviceProfile. That parameter shape is shared with #44, #45 and #46, so it should be settled once.Acceptance criterion: all six
DeviceProfilefields come from the description, the count band included. The SDK's arithmetic is already parameterized —openingFromRegister/openingToRegisterwork offprofile.registerPositionRange(), so a hardcoded band cannot exist inside the conversions — butprofiles::k2F85hardcodesopenPosition3 andclosedPosition230. A driver that passes the shipped profile has moved the hardcoding to another repo rather than removed it.Those two counts are installation state, not model constants: non-standard fingertips are common and meet before the linkage runs out of travel, so the closed end lands below 230 (robotiq/grippers#34 has the detail and the bench test that confirms it). Today
gripper_scaling.hpphardcodes 3 and 230 for every model and takesgripper_closed_positionas its only position parameter, so an installation whose fingers meet early gets compressed feedback and intermediate commands that overshoot — a half-closed command reaches for count 116 when half the real travel is nearer 101.The pattern already exists in the driver:
gripper_min_speed/gripper_max_speedarrive as<hardware>parameters rather than fromk2F85, because the description is the authority on the installation.openPosition/closedPositionshould arrive the same way, with the shipped profile as the fallback for anyone who has not measured.DeviceProfileis a plain struct with public members, so the driver can construct one from parameters and the SDK needs no change.The failure mode is quiet: the band inherits
k2F85while the parameterization is designed around opening, speed and force, and nobody notices until a gripper with custom tips goes to the wrong place.Numbers that are silently in the wrong unit once the command joint is metres:
goal_tolerance: 0.02(both controller configs)trigger_joint_command_threshold: 0.02(topic-based sim)max_velocity: 0.5/max_effort: 50.0initial_valueon the position state interface${gripper_closed_position}0.0<limit velocity="0.5" effort="50">per jointGRIPPER_JOINTSinrobotiq_control.launch.pyGazebo drops a link with no inertial, so the dummy link needs a token one or an
xacro:unless sim_gazebo. The extra joint also appears in the MoveIt Setup Assistant's joint list and in/joint_states; a chain-based group or an SRDF that names joints explicitly will not pick it up, but it is worth a comment in the xacro and a line in the README.The description tests are already parameterized on
(model, joint), so a rename flows through them;test_ros2_control_urdf.pyhas a mimic-joint test to extend.Blocks
#70 item 3, and with it the MCP's
units.py/robot_description.py.