primitive-agilex-piper-handeye-rbnx
Robonix package for hand-eye calibration TF publishing on the Piper + Orbbec Dabai DCW grasp pipeline. Vendors easy_handeye2 0.5.0.
Catalog name: robonix.primitive.agilex.piper.handeye.
⚠️ THE VENDORED CALIB IS A PLACEHOLDER (identity transform). Pick-and-place will silently miss every grasp until you replace
config/calibrations/my_eih_calib.calibwith a real calibration result. See Replacing the calibration.
What it does
Runs upstream easy_handeye2's handeye_publisher node, which broadcasts a single static TF on /tf_static based on a calibration YAML. In eye-in-hand mode (the default we ship), that's:
link6 → camera_color_optical_frame
link6is published byprimitive-agilex-piper-description-rbnx.camera_color_optical_frameis published byprimitive-orbbec-dabai_dcw-camera-rbnxonce the camera is up.
With this package running, the chain base_link → link1..link6 → camera_color_optical_frame is complete and downstream consumers (service-grasp-pose-rbnx, skill-pick-rbnx) can transform pixel detections into base-frame poses.
Capability surface
capabilities: []
Nothing routed on atlas. TF is a global ROS 2 side-channel (/tf_static); atlas-routing would add indirection with no benefit — every consumer uses tf2_ros::Buffer to look up transforms, not an atlas-routed RPC. This package just RegisterPrimitive on atlas so rbnx boot's wait_for_registration loop unblocks.
Driver-init lifecycle
There is no Driver(CMD_INIT) handler. The package registers a provider on atlas so rbnx boot proceeds, but does not receive a config JSON. start.sh:
- stages the calibration file into
~/.ros2/easy_handeye2/calibrations/<name>.calib(upstream hardcodes that path); - runs
ros2 launch easy_handeye2 publish.launch.py name:=<name>.
Calib staging quirk (important for debugging)
Upstream's easy_handeye2/easy_handeye2/__init__.py hardcodes:
CALIBRATIONS_DIRECTORY = pathlib.Path(
os.path.expanduser('~/.ros2/easy_handeye2/calibrations'))
…and handeye_publisher reads <CALIBRATIONS_DIRECTORY>/<name>.calib where <name> is the launch arg. We do not modify upstream (stays vendor-clean for future re-imports), so at start time atlas_register_and_launch.py copies config/calibrations/<name>.calib into ~/.ros2/easy_handeye2/calibrations/<name>.calib and then runs the launch.
If ~/.ros2/easy_handeye2/calibrations/<name>.calib already exists (operator placed a fresh calib there directly via the upstream calibration GUI), we leave it alone and warn — overwriting would clobber the operator's most recent calibration with a stale vendored copy. Set EASY_HANDEYE2_FORCE_STAGE=1 if you really do want to overwrite (i.e. you've intentionally committed a new calib to this package and want it to win).
Boot ordering
Independent of primitive-agilex-piper-description-rbnx — the two publish disjoint /tf_static subtrees. Recommend listing this after description just for readability.
Layout
primitive-agilex-piper-handeye-rbnx/
├── package_manifest.yaml
├── config/calibrations/
│ └── my_eih_calib.calib # ← YAML serialization of
│ # HandeyeCalibration msg
│ # (currently identity!)
├── scripts/
│ ├── build.sh # rbnx codegen + colcon build
│ ├── start.sh # source ROS + overlay, exec ↓
│ └── atlas_register_and_launch.py # register + stage calib +
│ # spawn `ros2 launch easy_handeye2
│ # publish.launch.py`
└── src/
├── easy_handeye2/ # upstream 0.5.0 (vendored)
└── easy_handeye2_msgs/ # upstream 0.5.0 (vendored)
Config / overrides
This package has no */driver capability, so rbnx boot v0.1 cannot deliver per-package config via Driver(CMD_INIT, config_json). The config: block in the deploy manifest is ignored. Use the top-level env: block instead:
env:
EASY_HANDEYE2_CALIB_NAME: my_eih_calib # default
EASY_HANDEYE2_CALIB_PATH: /abs/path/to.calib # absolute (wins)
EASY_HANDEYE2_FORCE_STAGE: "0" # overwrite ~/.ros2 staged
| env var | resolution priority | default | meaning |
|---|---|---|---|
EASY_HANDEYE2_CALIB_PATH |
1 (highest) | unset | absolute path to a .calib file. Basename (minus .calib) becomes the launch name:=. |
EASY_HANDEYE2_CALIB_NAME |
2 | my_eih_calib |
resolves to <pkg>/config/calibrations/<name>.calib. |
EASY_HANDEYE2_FORCE_STAGE |
— | 0 |
1 = overwrite ~/.ros2/easy_handeye2/calibrations/<name>.calib. |
ROBONIX_ATLAS |
— | 127.0.0.1:50051 |
atlas gRPC endpoint. |
Replacing the calibration
The shipped config/calibrations/my_eih_calib.calib is an identity transform with eye-in-hand metadata — robot_effector_frame: link6 / tracking_base_frame: camera_color_optical_frame. The TF it publishes will be zero-translation, identity-rotation. You must replace it.
Option A — Calibrate fresh on the real hardware (recommended)
Standard easy_handeye2 workflow. Requires the camera to actually see an ArUco marker mounted at tracking_marker_frame.
# 1. Bring up Piper + Orbbec + Piper URDF.
cd <deploy_dir>
rbnx boot
# 2. In another shell — run the upstream calibration GUI.
ros2 launch easy_handeye2 calibrate.launch.py \
calibration_type:=eye_in_hand \
name:=my_eih_calib \
robot_base_frame:=base_link \
robot_effector_frame:=link6 \
tracking_base_frame:=camera_color_optical_frame \
tracking_marker_frame:=tracking_marker
# 3. Take 10-20 samples (move arm to varied poses, click "Take
# Sample"), then "Compute" → "Save". Upstream writes:
# ~/.ros2/easy_handeye2/calibrations/my_eih_calib.calib
# 4. Copy that result back into this package:
cp ~/.ros2/easy_handeye2/calibrations/my_eih_calib.calib \
<this_package>/config/calibrations/
# 5. git commit + push. Other deploy machines pick up the new calib
# automatically next time they pull + rebuild.
Option B — Swap in a pre-existing calib file
cp my_existing.calib \
<this_package>/config/calibrations/my_eih_calib.calib
…or point at it via env (no copy needed):
env:
EASY_HANDEYE2_CALIB_PATH: /home/user/calibrations/my_existing.calib
The basename (minus .calib) becomes the launch name:=.
Verification
rbnx caps | grep easy_handeye2
# expect: easy_handeye2 provider, 0 capabilities
ros2 run tf2_ros tf2_echo link6 camera_color_optical_frame
# expect: a constant transform (the static calibration result)
# - with the placeholder calib, this is identity (translation 0,
# quaternion (0,0,0,1)) which means "camera at the center of
# link6 with no rotation" — physically wrong.
# Full chain check:
ros2 run tf2_ros tf2_echo base_link camera_color_optical_frame
# expect: a transform that varies as the arm moves (joint TF
# from RSP) but with the calibration step constant.
Frame contract
| frame | published by | role |
|---|---|---|
base_link |
primitive-agilex-piper-description-rbnx |
arm root |
link1..link6 |
primitive-agilex-piper-description-rbnx |
joint-driven |
camera_color_optical_frame |
primitive-orbbec-dabai_dcw-camera-rbnx |
RGB sensor frame |
link6 → camera_color_optical_frame |
this package | static hand-eye TF |
If tracking_base_frame in the calib differs from camera_color_optical_frame (e.g. you use the depth sensor frame instead), update both this calib AND the grasp-pose consumer's frame expectation.
Why we vendor instead of apt install ros-humble-easy-handeye2
- Reproducibility across deploy machines — vendoring guarantees byte-for-byte the same publisher behavior across different Jetsons.
- Calib file format pinning — the YAML schema is upstream's
HandeyeCalibrationmsg withset_message_fields. If upstream adds a field and we apt-upgrade, our placeholder + operator's old.calibfiles break. Vendoring isolates us from that. - No build surprises — upstream's
package.xmldeclarespython-transforms3d-pip,python3-opencv,rqt_guiasexec_dependfor the calibration GUI. We only runhandeye_publisher(which does not import any of those), so we silently tolerate them being absent on the build machine.
Multi-arm note
If two arms are present (future), each must have its own calib file with a unique name. Spawn this package twice in the deploy manifest with different EASY_HANDEYE2_CALIB_NAME per instance. The link6 / camera_color_optical_frame frame names will need to differ between instances too — that is a piper_description / OrbbecSDK URDF/namespace issue, outside this package's scope.
License
This package: MulanPSL-2.0. Vendored easy_handeye2 / easy_handeye2_msgs: see their respective LICENSE files.