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)
README primitive-linkerhand-o6-hand-rbnx@main

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] returns ok=false with a human-readable reason. Clamping would silently drive an axis somewhere the caller did not ask for.
  • ok=true means accepted and dispatched, not reached. Read back with get_state to 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:/dev and a device-cgroup rule for char major 188 (usb-serial).
  • pymodbus==3.5.1 and pyserial. pyserial must be installed explicitly: pymodbus 3.5.1 declares no dependencies at all, so it does not come along, and the failure surfaces as NameError: name 'serial' is not defined inside pymodbus — which reads like a pymodbus bug and is really a missing optional dep.
  • The vendored SDK needs its utils / core imports patched to LinkerHand.utils / LinkerHand.core when the consuming repo also has a top-level utils/ package (roboarm ships linkerhand-imports.patch for 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.