Back to packages

robonix.primitive.agilex.piper.handeye

Hand-eye calibration TF publisher (vendored easy_handeye2 0.5.0).

README

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.calib with 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
  • link6 is published by primitive-agilex-piper-description-rbnx.
  • camera_color_optical_frame is published by primitive-orbbec-dabai_dcw-camera-rbnx once 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:

  1. stages the calibration file into ~/.ros2/easy_handeye2/calibrations/<name>.calib (upstream hardcodes that path);
  2. 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 metadatarobot_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

  1. Reproducibility across deploy machines — vendoring guarantees byte-for-byte the same publisher behavior across different Jetsons.
  2. Calib file format pinning — the YAML schema is upstream's HandeyeCalibration msg with set_message_fields. If upstream adds a field and we apt-upgrade, our placeholder + operator's old .calib files break. Vendoring isolates us from that.
  3. No build surprises — upstream's package.xml declares python-transforms3d-pip, python3-opencv, rqt_gui as exec_depend for the calibration GUI. We only run handeye_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.