robonix.primitive.jaka.mini.arm_hand
primitive package
v0.1.0
JAKA 6-DOF arm + LinkerHand O6 dexterous hand primitive over one jkrc TCP session: arm contracts always, hand contracts in end_effector: hand mode (TG-9801 gripper mode retained).
primitive-jaka-mini_with_linker_hand_o6-arm_hand-rbnx — JAKA Mini arm + LinkerHand O6 primitive
Robonix primitive for the fixed-base JAKA Mini 6-DOF arm with a LinkerHand O6 six-axis dexterous hand, driven over TCP by the vendor jkrc SDK. One process owns the controller's single SDK session and its end RS485 channel, and serves two contract families from the same provider id:
| family | mode | contracts |
|---|---|---|
| arm | always | robonix/primitive/arm/{joint_states, joint_command, pos_command, end_pose} |
| hand | end_effector: hand |
robonix/primitive/hand/{info, move_joint, state_joint, set_joint_speed_limits, set_joint_torque_limits} |
end_effector: gripper (the default) keeps the TG-9801 behaviour and serves the
arm contracts only; the hand handlers answer ok=false ("hand mode not
enabled"). The deployment for the O6 sets end_effector: hand.
Why one package for arm + hand
The O6 hangs on the controller's end RS485L channel (channel 1, Modbus RTU,
slave 0x27 right / 0x28 left), behind the same jkrc connection the arm
uses. The controller allows one active SDK session: a second process that
logs in makes the first process's get_actual_joint_position hang
indefinitely (measured 2026-10-22), and the controller's RS485 signal table is
global across sessions and misaligns with more than one entry. Splitting the
hand into its own process is therefore not viable; the arm and the hand share
one driver, one lock and one signal table here.
Arm contracts
| contract | dir | topic | notes |
|---|---|---|---|
robonix/primitive/arm/joint_states |
out | /jaka/joint_states |
~50 Hz; arm joints (rad) + gripper_joint (m, gripper mode) + the six hand joints (rad, hand mode) |
robonix/primitive/arm/end_pose |
out | /jaka/end_pose |
TCP pose, base frame |
robonix/primitive/arm/joint_command |
in | /jaka/joint_command |
position-only write; a gripper_joint entry drives open/grip |
robonix/primitive/arm/pos_command |
in | /jaka/pos_command |
Cartesian target; IK on provider |
Hand contracts (end_effector: hand)
| contract | dir | topic | notes |
|---|---|---|---|
robonix/primitive/hand/info |
rpc | — | six self-describing axes (JointInfo) |
robonix/primitive/hand/move_joint |
rpc | — | position by axis name, normalized [0,1] (1 = flexed) |
robonix/primitive/hand/set_joint_speed_limits |
rpc | — | per-axis speed ceilings |
robonix/primitive/hand/set_joint_torque_limits |
rpc | — | per-axis torque ceilings |
robonix/primitive/hand/state_joint |
out | /jaka/hand/state_joint |
hand/JointState, normalized positions |
The O6 axes, in firmware order: thumb_cmc_pitch (thumb/bend),
thumb_cmc_yaw (thumb/oppose), index_mcp_pitch, middle_mcp_pitch,
ring_mcp_pitch, pinky_mcp_pitch (all bend). The distal joints are
mechanically coupled and are not exposed as separate axes. Positions are
normalized (0 = extended, 1 = fully flexed; thumb opposition 0 =
neutral, 1 = full); speed/torque limits are 0 = minimum, 1 = maximum.
move_joint rejects empty/unknown/duplicate/out-of-range requests and never
clamps; ok=true means accepted and dispatched, not reached.
At activation the hand pose is unknown: an RS485 read cannot be trusted as
a target seed (the O6 drops answers and a lost answer reads as 0 = fully
flexed), so the first move_joint must list all six axes — later requests may
be partial and hold the unlisted axes. The unchanged skill's gripper_joint
open/grip is a full six-axis pose and seeds the same cache, so it needs no
special first call.
One full six-axis RS485 sweep costs ~2.3 s (one single-entry poll per register),
so hand/state_joint publishes roughly one sweep per hand_publish_period_s
cycle. The O6 answers slowly (2~5 s) after a motion; a lost answer reads as 0,
so a sweep during that window can briefly show a moving finger as fully
flexed. Judge grasp results after the motion settles.
The unchanged JAKA skill
In hand mode a gripper_joint entry in joint_command is still the skill's
grip interface: values at/above half the 98 mm stroke open the O6, values below
half run the full-grasp pose. The skill's five-publish burst is idempotent
(each publish re-issues the same open/grip command; the O6 settles on the same
pose). The skill reconstructs a width-like opening from those hand joints
(thumb/index/middle/ring flexion, the same 0.098 m scale and verdict
thresholds), so its empty-grasp/open checks keep working without a
gripper_joint entry. The O6's lost-answer clusters can still look fully
closed — the skill requires settled samples, but judge critical grasps with
care. The hand joints are also on /jaka/hand/state_joint and in
joint_states.
Configuration
See config.spec. Essentials:
ip— JAKA controller address.end_effector—gripper(TG-9801, default) orhand(O6).hand_type—right(0x27) orleft(0x28), hand mode only.motion_enabled— safety gate (default false); gates both arm and hand motions.gripper_force/gripper_speed— TG-9801 only (ignored in hand mode).hand_publish_period_s— hand state stream dwell.shutdown_home_joints— on teardown, the hand opens first, then the arm moves here and powers off.
Safety
motion_enabled: falseby default: boot read-only, verifyjoint_states,end_pose,hand/infoandhand/state_joint, then enable.- Clear the workspace before commanding the O6; a grasp closes all five
fingers. Lower the torque ceiling with
hand/set_joint_torque_limitsfor the first live grasp. - No other SDK client and no other RS485 owner may connect to the controller while this provider runs (a second login breaks the active session).
Build
rbnx build -p /path/to/primitive-jaka-mini_with_linker_hand_o6-arm_hand-rbnx
Creates a package-local venv (--system-site-packages, so system rclpy is
visible), generates gRPC/MCP/ROS 2 types, and colcon-builds the ROS 2 message
overlay that scripts/start.sh sources.
Tests
.venv/bin/python -m unittest discover -s jaka_arm/tests -t .
41 tests cover the arm driver (fake jkrc), the TG-9801 adapter and the O6 hand driver/contract mapping; no hardware is required.
Bench smoke 2026-10-22 (JAKA Mini + right O6): one jkrc session served both
families, joint_states carried 6 arm + 6 hand joints, hand/info returned
six axes, a partial move_joint was rejected until a full six-axis target was
sent, and the arm feedback stayed healthy (no -3 reads) while the hand moved.
The O6's lost-answer clusters still show up as brief 1.0 (fully flexed)
readings in the state streams — read a verdict after the motion settles.
JAKA SDK binaries
The aarch64 jkrc SDK (jkrc.so, libjakaAPI.so, pyV2.1.7.0) ships in this
repository — Apache-2.0, added unmodified from the JAKA official download;
see NOTICE and jaka_arm/jaka_sdk/LICENSE.
For other platforms (x86_64-linux-gnu, windows), download the SDK from
the JAKA official download page and place both files under
jaka_arm/jaka_sdk/<platform>/; scripts/build.sh fails fast with these
instructions when they are missing.
License
Apache-2.0. jaka_arm/driver.py, jaka_arm/gripper.py and
jaka_arm/hand.py carry MIT-adapted AIIT-Roboarm code — see
NOTICE.