robonix.primitive.linkerhand.o6.hand
primitive package
v0.1.0
LinkerHand O6 dexterous-hand primitive — joint-level position/speed/torque control over Modbus RTU (RS485), with an atlas-routed ROS 2 state stream.
6 issues found while indexing this entry
- capability robonix/primitive/hand/driver declares no transport (mcp, grpc or ros2)
- capability robonix/primitive/hand/info declares no transport (mcp, grpc or ros2)
- capability robonix/primitive/hand/move_joint declares no transport (mcp, grpc or ros2)
- capability robonix/primitive/hand/set_joint_speed_limits declares no transport (mcp, grpc or ros2)
- capability robonix/primitive/hand/set_joint_torque_limits declares no transport (mcp, grpc or ros2)
- capability robonix/primitive/hand/get_state declares no transport (mcp, grpc or ros2)
primitive-piper_with_linkerhand-hand-rbnx
Robonix primitive for the LinkerHand O6 six-axis dexterous hand over Modbus
RTU (RS485). Implements the global joint-level robonix/primitive/hand/*
contracts, served over gRPC.
Catalog name: robonix.primitive.linkerhand.o6.hand.
Capability surface
| Contract | Mode | Transport | Notes |
|---|---|---|---|
robonix/primitive/hand/driver |
rpc | gRPC | Lifecycle (wired to on_init / on_activate) |
robonix/primitive/hand/info |
rpc | gRPC | Control-axis layout — call this before driving axes |
robonix/primitive/hand/move_joint |
rpc | gRPC | Set axis positions by name |
robonix/primitive/hand/set_joint_speed_limits |
rpc | gRPC | Per-axis speed limit by name |
robonix/primitive/hand/set_joint_torque_limits |
rpc | gRPC | Per-axis torque limit by name |
robonix/primitive/hand/get_state |
rpc | gRPC | Read current positions (package-local, see below) |
The first five resolve against the robonix source tree's
capabilities/primitive/hand/; get_state is package-local
(capabilities/primitive/hand/get_state.v1.toml + its IDL).
Units
Everything crossing the gRPC surface is normalized [0,1] per axis:
| 0 | 1 | |
|---|---|---|
| position | open | closed |
| speed | slowest | fastest |
| torque | least | most |
The O6 firmware speaks raw 0–255, and position is inverted against the
contract — the firmware uses 255 = fully open. Speed and torque are not
inverted. So the conversion is 1 - raw/255 for position and raw/255 for
limits; getting position backwards opens the hand when told to close, which is
why it lives in exactly two functions (hand_ctl/main.py:_pos_to_raw,
_raw_to_pos) and nowhere else.
Axis layout
info returns six self-describing axes, in firmware order:
| name | finger | type | joint_index |
|---|---|---|---|
thumb_cmc_pitch |
thumb | bend | 0 |
thumb_cmc_yaw |
thumb | oppose | 0 |
index_mcp_pitch |
index | bend | 0 |
middle_mcp_pitch |
middle | bend | 0 |
ring_mcp_pitch |
ring | bend | 0 |
pinky_mcp_pitch |
pinky | bend | 0 |
Address axes by name — never parse the name for meaning. The DIP/IP joints are
not exposed: the hand drives them mechanically, coupled to the MCP joint.
Call semantics
move_joint and the two set_joint_*_limits follow the contract's
accept semantics:
- Only listed axes change; unlisted axes hold their last target.
- Rejects, never clamps. An empty request, an unknown axis name, a duplicate
name, a non-finite value or anything outside
[0,1]returnsok=falsewith a human-readable reason. Clamping would silently drive an axis somewhere the caller did not ask for. ok=truemeans accepted and dispatched, not reached. Read back withget_stateto see where the fingers actually are.
Also part of the semantics: a full pose is written in one Modbus frame, so the fingers never pass through an unintended intermediate shape.
Why no state_joint
The global hand set covers readback as the topic_out contracts state_joint /
state_finger, whose IDL is hand/msg/*.msg — that directory is a codegen
search root, not a buildable ROS package. Serving those would mean vendoring a
copy of a global message definition and colcon-building it, i.e. owning a fork
that silently drifts when upstream changes the message.
get_state is the on-demand, axis-level equivalent. The sibling package
syswonder/primitive-linkerbot-linker_hand_o6-hand-rbnx reaches the same
conclusion for the same reason (it declares get_state too, though without
shipping the IDL for it — capabilities/lib/hand/srv/GetState.srv here is that
missing IDL).
Relationship to the CAN sibling
syswonder/primitive-linkerbot-linker_hand_o6-hand-rbnx implements the same
contracts for the same hand over CAN. This package exists because the hand on
this robot is wired to a USB-RS485 adapter and speaks Modbus RTU. Everything a
consumer can observe is deliberately identical — axis names, axis order, units,
accept/reject semantics, which contracts are and are not served — so the two are
interchangeable on atlas.
Config (via Driver(CMD_INIT, config_json))
| key | default | meaning |
|---|---|---|
sdk_path |
"" |
Path to the vendored linkerhand-python-sdk checkout. The SDK is not a pip package, so it ships as source. |
hand_type |
right |
right = slave/CAN id 0x27, left = 0x28. Also selects the default gesture tables upstream. |
hand_joint |
O6 |
Only O6 is supported — this package is O6-specific (register map + axis layout). |
modbus_port |
/dev/ttyUSB0 |
RS485 adapter. Empty string selects the SDK's CAN path instead. |
can_channel |
can0 |
Only used when modbus_port is empty. |
speed |
[255]*6 |
Initial per-axis speed, firmware 0–255. |
torque |
[200]*6 |
Initial per-axis torque, firmware 0–255. |
sentinel_timeout_s |
30.0 |
How long on_activate waits for the first successful register read. |
Requirements
- The hand reachable on a serial port — in a container this needs
-v /dev:/devand a device-cgroup rule for char major 188 (usb-serial). pymodbus==3.5.1andpyserial.pyserialmust be installed explicitly: pymodbus 3.5.1 declares no dependencies at all, so it does not come along, and the failure surfaces asNameError: name 'serial' is not definedinside pymodbus — which reads like a pymodbus bug and is really a missing optional dep.- The vendored SDK needs its
utils/coreimports patched toLinkerHand.utils/LinkerHand.corewhen the consuming repo also has a top-levelutils/package (roboarm shipslinkerhand-imports.patchfor this).
Build & run
rbnx build -p . # codegen: proto + gRPC stubs
rbnx start -p . # or via a deploy manifest, which is the normal path
rbnx boot sends Driver(CMD_INIT, config_json), then CMD_ACTIVATE, which is
what actually opens the serial port; CMD_DEACTIVATE / CMD_SHUTDOWN close it.