Robonix package wrapping the AgileX Piper 6-DoF arm hardware driver. It
provides the original ROS 2 backend and a direct piper_sdk backend behind one
package identity and one primitive/arm/* namespace.
Catalog name: robonix.primitive.agilex.piper.arm.
Naming note: internal directory / binary name is piper_ctl (_ctl = low-level control), while the contract namespace is arm. Piper is an arm, so calling this piper_chassis_rbnx would mislead.
Backends
Set backend in the deployment instance’s config block:
Backend
Default
Control surface
ros2
yes
Original ROS topics for joint feedback, joint/Cartesian commands and Piper status
sdk
no
MCP arm/gripper control, absolute joint trajectories and Vitals health telemetry
The default remains ros2; existing deployment manifests therefore keep the
same startup and runtime behavior. Robonix delivers instance configuration only
after the package process registers through Driver(CMD_INIT), so
piper_ctl.main performs the backend dispatch at that lifecycle boundary.
The five standard contracts (driver / joint_states / joint_command / end_pose / pos_command) mirror robonix’s global capabilities/primitive/arm/*.v1.toml: same id, version, IDL, and mode. Keep them byte-compatible with the global tree so a future consolidation is a no-op.
arm_status is a Piper-specific vendor extension and remains package-local. Its IDL piper_msgs/PiperStatusMsg.msg is shipped at package level under capabilities/lib/piper_msgs/msg/PiperStatusMsg.msg.
Single-source-of-truth for the PiperStatusMsg IDL is the vendored src/piper_msgs/msg/ directory. The mirrored copy under capabilities/lib/piper_msgs/msg/ is what rbnx codegen / atlas’s contract registry actually scan. Keep the two in sync — plain file copies are used rather than symlinks to keep git diff legible.
Operator-authorized runtime lock/unlock; intentionally absent from MCP
robonix/primitive/health/state / stream
gRPC
Vitals-compatible telemetry
set_enabled remains as a backward-compatible arm-enable operation. The SDK
backend validates finite inputs, Piper hard limits, speed, waypoint count,
feedback age, settling tolerance and hardware faults. Motion is locked by
default. allow_motion, ROBONIX_ALLOW_MOTION, and the per-arm override select
only the initial state at activation. Set them to true only after clearing the
workspace and checking the emergency stop. Runtime locking immediately closes
the software motion gate and sends Piper’s fast stop, including during an active
trajectory. Runtime unlocking is not an MCP tool and requires the local token
generated with mode 0600 for the current provider activation. See
CAPABILITY.md for its complete behavioral contract.
Use the dedicated operator script against the deployment provider id:
When the runtime gate is locked, SDK MCP status and lock-rejected motion
responses include an absolute, provider-specific version of the unlock command
in detail. This lets Pilot relay the exact command while the gRPC-only contract
and owner-readable token continue to prevent Pilot from unlocking the arm.
Locking never requires a token. Unlocking reads
rbnx-build/data/<provider>.motion-token; the token is replaced on every SDK
backend activation and removed on deactivate or shutdown. The script rejects a
token file owned by another user or readable by group/other users. Set
ROBONIX_ATLAS when the operator shell does not use the default Atlas endpoint.
One-time gripper zeroing
Gripper zeroing is required only when neither Piper feedback nor the current
provider session has an established zero; it is not repeated before every
gripper movement. Perform this operator workflow on the selected provider:
Call disable_gripper with an empty request {}.
Read status and verify gripper_enabled=false.
Manually close the disabled gripper to its mechanical closed position.
Call set_gripper_zero with {"fully_closed_confirmed": true}.
Call enable_gripper, or send the next set_gripper request and let it
enable the gripper automatically.
The six arm joint drivers may remain enabled throughout this workflow because
arm and gripper enable states are controlled independently. set_gripper_zero
rejects an enabled gripper with an instruction to call disable_gripper first;
on rejection it sends no gripper command. A successful zero operation verifies
the feedback and leaves the gripper disabled.
Joint-space vs Cartesian control paths
Two independent command channels; each one bypasses the other:
Joint-space (default in the vertical-grasp deploy): consumers publish sensor_msgs/JointState on arm/joint_command. roboarm_ik already solves IK, so the driver just forwards the target angles to the Piper SDK’s JointCtrl + GripperCtrl. The gripper entry in the JointState carries finger opening.
Cartesian: consumers publish geometry_msgs/Pose on arm/pos_command. The driver converts the quaternion to xyz-euler and forwards to the Piper SDK’s EndPoseCtrl (SDK-side / firmware IK). Gripper is NOT part of this contract — command it separately through joint_command.
Boot ordering
Boot this before any consumer of primitive/arm/*. In the vertical-grasp pipeline:
primitive-agilex-piper-description-rbnx consumes arm/joint_states to drive robot_state_publisher;
service-roboarm-ik-rbnx consumes arm/joint_states and publishes to arm/joint_command (default path);
service-piper-moveit-rbnx consumes arm/arm_status and publishes to arm/pos_command (Cartesian path);
skill-pick-vertical-grasp-rbnx polls arm/arm_status between grasps and monitors /arm/joint_states_single to verify that the gripper is holding an object.
rbnx-cli has no defer/retry, so providers MUST come first in YAML declaration order.
Driver-init lifecycle
start.sh brings up one shared provider process. It does not connect CAN or
spawn ROS. The provider registers primitive/arm/driver, then blocks on
Driver(CMD_INIT, config_json).
When rbnx boot invokes Init it passes the manifest’s config: block as JSON. The lifecycle then runs:
CMD_INIT: choose backend (default ros2) and validate only that backend’s configuration;
CMD_ACTIVATE with ros2: optionally set up CAN, spawn the ROS driver,
wait for JointState, then declare the five ROS data topics;
CMD_ACTIVATE with sdk: import piper_sdk, connect the configured CAN
interface and publish the MCP/health endpoints without enabling or moving hardware.
CMD_DEACTIVATE / CMD_SHUTDOWN release the selected backend idempotently:
the ROS backend kills its process group, while the SDK backend disconnects and
applies its configured shutdown stop policy.
CAN bring-up — TWO paths
The Piper arm uses USB-CAN (MCP251xFD or similar). The CAN interface must be up and named can_piper (or whatever you set can_port: to) before piper_ctrl_single_node can talk to it. The CAN interface name is config-driven — the package never hardcodes a device name.
If the configured USB address or interface name looks wrong, inspect
the current host mapping first:
bash scripts/find_all_can_port.sh
Use the reported USB bus path, for example 1-4.2:1.0, as
can_usb_address. Do not guess when multiple CAN interfaces are
present; Jetson onboard *.mttcan interfaces and USB-CAN adapters can
coexist.
Path A — recommended for production (default)
Bring can_piper up outside rbnx boot, ahead of time. On a Jetson with udev rules, this is once-per-boot:
# Run once per host boot, BEFORE `rbnx boot`:
bash scripts/can_activate.sh can_piper 1000000 "1-4.2:1.0"
# Replace "1-4.2:1.0" with the USB bus path of your Piper.
# To list candidates:
# lsusb -t
# sudo ethtool -i can0 | grep bus-info
auto_can_setup stays false in the manifest. This path keeps the deploy free of sudo coupling — the operator approves the sudo prompt once, interactively, then rbnx boot runs unattended.
Path B — convenience for dev laptops
Set auto_can_setup: true in the manifest config block. on_activate will run scripts/can_activate.sh itself before spawning the driver. Requires passwordless sudo for the operator (otherwise the script blocks on the prompt and CMD_ACTIVATE times out).
backend: ros2 # default; preserves original behavior
can_port: can_piper # CAN interface name
can_bitrate: 1000000
can_usb_address: "" # pin via USB bus path; "" = first detected
auto_can_setup: false # see "CAN bring-up" above
auto_enable: true # forwarded to start_single_piper auto_enable
gripper_exist: true
gripper_val_mutiple: 2 # upstream typo preserved
arm_namespace: /arm # see note in main.py docstring
sentinel_timeout_s: 30.0 # max wait for first JointState in on_activate
# Topic-name overrides (rarely needed; default derives from arm_namespace):
# joint_states_topic: /arm/joint_states_single
# joint_command_topic: /arm/joint_command
# arm_status_topic: /arm/arm_status
# end_pose_topic: /arm/end_pose
# pos_command_topic: /arm/pos_command
Selecting a backend in a deployment
Select the backend in the deployment repository’s robonix_manifest.yaml,
under the config: block of each Piper primitive instance. Do not edit this
package’s config.spec to select a backend, and do not put backend in
.env. If backend is omitted, the instance uses ros2.
A dual-arm direct-SDK deployment is configured as follows:
gripper_zero_confirmation_tolerance_m is the maximum absolute feedback
accepted after the SDK sends 0xAE. It defaults to 0.0005 m and may be tuned
independently for each physical gripper, but it must remain between 0.0001 m
and that instance’s gripper_zero_max_offset_m. Use the smallest value supported
by measured hardware feedback: increasing it can accept a residual value that
would otherwise reveal an ignored zero command.
scripts/build.sh automatically fetches the tested piper_sdk 0.2.20 source
at commit 4eddfcf817cd87de9acee316a72cf5b988025378. The checkout is stored under
rbnx-build/piper_sdk_parent/, validated without opening CAN, and selected by
scripts/start.sh; no host-level SDK installation or manual PYTHONPATH is
required.
For controlled offline builds, set RBNX_PIPER_SDK_PARENT to an existing
directory that contains a piper_sdk/ checkout. To skip SDK preparation on a
ROS-only host, set RBNX_PIPER_SDK_BUILD=0; starting the SDK backend then
requires the caller to provide PIPER_SDK_PARENT. The repository and full
commit SHA may be overridden for a mirror or a deliberately tested revision:
To drive the lifecycle manually (without rbnx boot), call the arm’s Driver service with CMD_INIT and a JSON config blob, then call CMD_ACTIVATE. Init only validates configuration; Activate returns after the first JointState is observed and the five data topics are declared.
src/piper/, src/piper_msgs/, src/graspnet_msgs/ are verbatim copies from agilexrobotics/piper_ros. The other packages in the upstream workspace are deliberately not vendored here and land in their own robonix packages:
piper_gazebo / piper_mujoco / piper_sim → not migrated (sim-only).
If anything diverges from upstream, drop a *.patch alongside src/ documenting the diff.
License
The existing package and ROS integration are Apache-2.0. The contributed
direct-SDK implementation retains its MIT notice in LICENSE-SDK.
Vendored piper_ros / piper_msgs / graspnet_msgs keep their respective licenses.
primitive-agilex-piper-arm-rbnx
Robonix package wrapping the AgileX Piper 6-DoF arm hardware driver. It provides the original ROS 2 backend and a direct
piper_sdkbackend behind one package identity and oneprimitive/arm/*namespace.Catalog name:
robonix.primitive.agilex.piper.arm.Backends
Set
backendin the deployment instance’sconfigblock:ros2sdkThe default remains
ros2; existing deployment manifests therefore keep the same startup and runtime behavior. Robonix delivers instance configuration only after the package process registers throughDriver(CMD_INIT), sopiper_ctl.mainperforms the backend dispatch at that lifecycle boundary.Capability surface
ROS 2 backend
robonix/primitive/arm/driverDriver(CMD_INIT, config_json)— lifecycle gaterobonix/primitive/arm/joint_states/<ns>/joint_states_single(sensor_msgs/JointState)robonix/primitive/arm/joint_command/<ns>/joint_command(sensor_msgs/JointState) — driver subscribes; consumers publishrobonix/primitive/arm/end_pose/<ns>/end_pose(geometry_msgs/Pose)robonix/primitive/arm/pos_command/<ns>/pos_command(geometry_msgs/Pose) — driver subscribes; consumers publishrobonix/primitive/arm/arm_status/<ns>/arm_status(piper_msgs/PiperStatusMsg) — vendor extensionThe five standard contracts (driver / joint_states / joint_command / end_pose / pos_command) mirror robonix’s global
capabilities/primitive/arm/*.v1.toml: same id, version, IDL, and mode. Keep them byte-compatible with the global tree so a future consolidation is a no-op.arm_statusis a Piper-specific vendor extension and remains package-local. Its IDLpiper_msgs/PiperStatusMsg.msgis shipped at package level undercapabilities/lib/piper_msgs/msg/PiperStatusMsg.msg.Direct-SDK backend
robonix/primitive/arm/statusrobonix/primitive/arm/enable_arm/disable_armrobonix/primitive/arm/enable_gripper/disable_gripperrobonix/primitive/arm/jog_jointrobonix/primitive/arm/execute_joint_trajectoryrobonix/primitive/arm/set_gripperrobonix/primitive/arm/set_gripper_zerorobonix/primitive/arm/emergency_stoprobonix/primitive/arm/motion_lockrobonix/primitive/health/state/streamset_enabledremains as a backward-compatible arm-enable operation. The SDK backend validates finite inputs, Piper hard limits, speed, waypoint count, feedback age, settling tolerance and hardware faults. Motion is locked by default.allow_motion,ROBONIX_ALLOW_MOTION, and the per-arm override select only the initial state at activation. Set them to true only after clearing the workspace and checking the emergency stop. Runtime locking immediately closes the software motion gate and sends Piper’s fast stop, including during an active trajectory. Runtime unlocking is not an MCP tool and requires the local token generated with mode0600for the current provider activation. SeeCAPABILITY.mdfor its complete behavioral contract.Use the dedicated operator script against the deployment provider id:
When the runtime gate is locked, SDK MCP
statusand lock-rejected motion responses include an absolute, provider-specific version of the unlock command indetail. This lets Pilot relay the exact command while the gRPC-only contract and owner-readable token continue to prevent Pilot from unlocking the arm.Locking never requires a token. Unlocking reads
rbnx-build/data/<provider>.motion-token; the token is replaced on every SDK backend activation and removed on deactivate or shutdown. The script rejects a token file owned by another user or readable by group/other users. SetROBONIX_ATLASwhen the operator shell does not use the default Atlas endpoint.One-time gripper zeroing
Gripper zeroing is required only when neither Piper feedback nor the current provider session has an established zero; it is not repeated before every gripper movement. Perform this operator workflow on the selected provider:
disable_gripperwith an empty request{}.statusand verifygripper_enabled=false.set_gripper_zerowith{"fully_closed_confirmed": true}.enable_gripper, or send the nextset_gripperrequest and let it enable the gripper automatically.The six arm joint drivers may remain enabled throughout this workflow because arm and gripper enable states are controlled independently.
set_gripper_zerorejects an enabled gripper with an instruction to calldisable_gripperfirst; on rejection it sends no gripper command. A successful zero operation verifies the feedback and leaves the gripper disabled.Joint-space vs Cartesian control paths
Two independent command channels; each one bypasses the other:
sensor_msgs/JointStateonarm/joint_command.roboarm_ikalready solves IK, so the driver just forwards the target angles to the Piper SDK’sJointCtrl+GripperCtrl. Thegripperentry in the JointState carries finger opening.geometry_msgs/Poseonarm/pos_command. The driver converts the quaternion to xyz-euler and forwards to the Piper SDK’sEndPoseCtrl(SDK-side / firmware IK). Gripper is NOT part of this contract — command it separately throughjoint_command.Boot ordering
Boot this before any consumer of
primitive/arm/*. In the vertical-grasp pipeline:primitive-agilex-piper-description-rbnxconsumesarm/joint_statesto driverobot_state_publisher;service-roboarm-ik-rbnxconsumesarm/joint_statesand publishes toarm/joint_command(default path);service-piper-moveit-rbnxconsumesarm/arm_statusand publishes toarm/pos_command(Cartesian path);skill-pick-vertical-grasp-rbnxpollsarm/arm_statusbetween grasps and monitors/arm/joint_states_singleto verify that the gripper is holding an object.rbnx-cli has no defer/retry, so providers MUST come first in YAML declaration order.
Driver-init lifecycle
start.shbrings up one shared provider process. It does not connect CAN or spawn ROS. The provider registersprimitive/arm/driver, then blocks onDriver(CMD_INIT, config_json).When
rbnx bootinvokes Init it passes the manifest’sconfig:block as JSON. The lifecycle then runs:CMD_INIT: choosebackend(defaultros2) and validate only that backend’s configuration;CMD_ACTIVATEwithros2: optionally set up CAN, spawn the ROS driver, wait forJointState, then declare the five ROS data topics;CMD_ACTIVATEwithsdk: importpiper_sdk, connect the configured CAN interface and publish the MCP/health endpoints without enabling or moving hardware.CMD_DEACTIVATE/CMD_SHUTDOWNrelease the selected backend idempotently: the ROS backend kills its process group, while the SDK backend disconnects and applies its configured shutdown stop policy.CAN bring-up — TWO paths
The Piper arm uses USB-CAN (MCP251xFD or similar). The CAN interface must be up and named
can_piper(or whatever you setcan_port:to) beforepiper_ctrl_single_nodecan talk to it. The CAN interface name is config-driven — the package never hardcodes a device name.If the configured USB address or interface name looks wrong, inspect the current host mapping first:
Use the reported USB bus path, for example
1-4.2:1.0, ascan_usb_address. Do not guess when multiple CAN interfaces are present; Jetson onboard*.mttcaninterfaces and USB-CAN adapters can coexist.Path A — recommended for production (default)
Bring
can_piperup outsiderbnx boot, ahead of time. On a Jetson with udev rules, this is once-per-boot:auto_can_setupstaysfalsein the manifest. This path keeps the deploy free of sudo coupling — the operator approves the sudo prompt once, interactively, thenrbnx bootruns unattended.Path B — convenience for dev laptops
Set
auto_can_setup: truein the manifest config block.on_activatewill runscripts/can_activate.shitself before spawning the driver. Requires passwordless sudo for the operator (otherwise the script blocks on the prompt andCMD_ACTIVATEtimes out).Path B is more convenient when the USB-CAN’s bus path shifts between sessions, but couples the deploy to sudoers state — not ideal for production.
Layout
Config (passed via
Driver(CMD_INIT, config_json))Selecting a backend in a deployment
Select the backend in the deployment repository’s
robonix_manifest.yaml, under theconfig:block of each Piper primitive instance. Do not edit this package’sconfig.specto select a backend, and do not putbackendin.env. Ifbackendis omitted, the instance usesros2.A dual-arm direct-SDK deployment is configured as follows:
gripper_zero_confirmation_tolerance_mis the maximum absolute feedback accepted after the SDK sends0xAE. It defaults to0.0005m and may be tuned independently for each physical gripper, but it must remain between0.0001m and that instance’sgripper_zero_max_offset_m. Use the smallest value supported by measured hardware feedback: increasing it can accept a residual value that would otherwise reveal an ignored zero command.scripts/build.shautomatically fetches the testedpiper_sdk0.2.20 source at commit4eddfcf817cd87de9acee316a72cf5b988025378. The checkout is stored underrbnx-build/piper_sdk_parent/, validated without opening CAN, and selected byscripts/start.sh; no host-level SDK installation or manualPYTHONPATHis required.For controlled offline builds, set
RBNX_PIPER_SDK_PARENTto an existing directory that contains apiper_sdk/checkout. To skip SDK preparation on a ROS-only host, setRBNX_PIPER_SDK_BUILD=0; starting the SDK backend then requires the caller to providePIPER_SDK_PARENT. The repository and full commit SHA may be overridden for a mirror or a deliberately tested revision:All SDK limits and timeout defaults are documented in
config.spec.Build / run standalone
Offline validation (does not open CAN or command hardware):
To drive the lifecycle manually (without
rbnx boot), call the arm’sDriverservice withCMD_INITand a JSON config blob, then callCMD_ACTIVATE. Init only validates configuration; Activate returns after the first JointState is observed and the five data topics are declared.Verification
Vendor / upstream
src/piper/,src/piper_msgs/,src/graspnet_msgs/are verbatim copies from agilexrobotics/piper_ros. The other packages in the upstream workspace are deliberately not vendored here and land in their own robonix packages:piper_description/piper_with_gripper_moveit→primitive-agilex-piper-description-rbnx.piper_humble/piper_moveit_control→service-piper-moveit-rbnx.piper_gazebo/piper_mujoco/piper_sim→ not migrated (sim-only).If anything diverges from upstream, drop a
*.patchalongsidesrc/documenting the diff.License
The existing package and ROS integration are Apache-2.0. The contributed direct-SDK implementation retains its MIT notice in
LICENSE-SDK. Vendored piper_ros / piper_msgs / graspnet_msgs keep their respective licenses.