Skip to content
AuthorGiovanni DateAugust 14, 2026 Rev1.0

Grasp geometry is base_link math; detections are odom

The fact: a detected object's pose and the pose a grasp is computed from are in two different frames, on purpose, and they are not interchangeable. Substituting one for the other does not raise an error — it produces a plausible, wrong grasp.

Why each frame is what it is

Frame Why
A stored detection (detect_objects, estimate_object_pose_fine, the _segmented_objects registry) odom A detection has to survive the base driving to it. An odom pose is still correct after the robot moves; a base_link one silently is not.
Grasp geometry (grasp-service's compute_grasp_pose, the robot's _detect_top) base_link It is all robot-relative: which arm is the sign of the object's y, and the side approach heading is atan2(y, x) from base_link's origin.

What goes wrong

Neither of those quantities can fail loudly on the wrong frame. Both are defined for any pose you hand them. An odom pose fed to grasp geometry yields:

  • the wrong arm, whenever the robot's odom origin and its current position put the object on opposite sides of y = 0 — which is most of the time, since odom's origin is wherever the robot booted; and
  • the wrong approach heading, since atan2(y, x) is measured from the robot, and odom measures from the boot pose.

The grasp then runs to completion and misses, or reaches across the body for an object beside the other hand. The first visible symptom is a failed held-check, several steps and one arm movement later, with nothing in the logs naming a frame.

How the stack handles it

  • The robot returns both: pose_odom (the stored detection, unchanged) and pose_base_link (computed fresh per call, never stored — a cached base_link pose goes stale the moment the base moves, which is the whole reason detections are odom).
  • pose_base_link is None, never a guess, when TF is unavailable.
  • grasp-service's Perception and DetectedObject carry the two as separate fields, so no code path can reach for "the pose" and get the wrong one by default.
  • The unified grasp FSM refuses an object with no pose_base_link — reporting unreachable before it touches the robot — rather than falling back to the odom pose. A robot that cannot supply base_link is saying it cannot do a geometric grasp.

If you are adding a grasp path

Take the base_link pose explicitly and fail closed when it is missing. The temptation is a pose_base_link or pose fallback, because it makes the type-checker happy and the tests pass on a robot that always supplies both. It is precisely the bug: it converts "I cannot do this" into "I did something plausible."

Recorded from BIN-324 / BIN-325. The latent case was real — grasp-service's quick-tier _run_side_quick fed perceive()'s odom pose straight into compute_grasp_pose.