Layer 1 — Hardware Abstraction (HAL)
Part of the OpenRAL public-symbol inventory. Hand-curated;
(LNN)markers are refreshed bytools/refresh_methods_linenos.py.
python/hal/src/openral_hal/protocol.py
Normative HAL protocol plus explicit optional lifecycle extensions.
class HAL(Protocol)(L89) — Structural protocol every HAL adapter must satisfy.- attr
description: RobotDescription connect() -> None(L106) — Open connection to robot/sim.disconnect() -> None(L116) — Close connection (idempotent).read_state() -> JointState(L124) — Latest joint state snapshot (hot path).send_action(action: Action) -> None(L138) — Forward action chunk to controller (hot path).estop() -> None(L154) — Trigger emergency stop, always raisesROSEStopRequested.class LifecycleEStopHAL(Protocol)(L64) — Opt-in propagation of the generic lifecycle e-stop to downstream hardware or owned processes.estop() -> None(L74) — Propagate the generic lifecycle e-stop to downstream hardware / owned processes.class ResettableLifecycleEStopHAL(LifecycleEStopHAL, Protocol)(L80) — Opt-in in-process recovery contract.reset_estop() -> None(L83) — In-process recovery from a resettable e-stop.class EStopRecovery(StrEnum)(L35) — Declares whether recovery is resettable or requires a full lifecycle restart.class HALHealthReport(L43) — Cached, I/O-free health payload (message + fields) for the lifecycle diagnostics heartbeat.class HALHealthProvider(Protocol)(L55) — Opt-in health-reporting protocol.health() -> HALHealthReport(L58) — Cached, I/O-free diagnostics consumed by the generic lifecycle heartbeat.
python/hal/src/openral_hal/_mujoco_arm.py
Internal MuJoCo-backed HAL implementation shared by UR / Franka / SO-100 / G1 / H1 / Rizon-4 / OpenArm / ALOHA adapters. Reads its wiring from RobotDescription.sim.
class _MujocoArmInitKwargs(TypedDict)— Typed shape of the kwargsMujocoArmHAL.__init__accepts. (L64) Lets_sim_kwargs_forbuild a value that unpacks undermypy --strictwith no# type: ignore._resolve_mjcf_path(desc: RobotDescription) -> str[private] — Resolvesdesc.assets.mjcfto an absolute MJCF path viaopenral_core.assets.resolve_asset; raisesROSConfigErrorif unset/unresolvable. (L84)_kinematic_group(joint_name, groups, *, robot) -> str[private] (L114) — Maps a joint name to its kinematic group (substring match againstgroups) for the G1/H1 humanoid velocity/effort/PD-gain lookup tables; raisesROSConfigErrornamingrobotif none match.build_hal(description, *, mode: Literal["sim","real"], transport=None, sim_env_yaml=None) -> HAL(resolver.pyL39) — The single seam for building a robot's sim or real HAL from its manifest, used bydeploy sim/deploy run.modepicksdescription.hal.sim/.real(or a scene YAML viasim_env_yaml), merged underhal.parameters.defaults, withsafety.joint_state_staleness_limit_s(when declared) passed as the HAL'sstaleness_limit_s— sim-derived HALs included — andtransporton top; raisesROSConfigError/ROSCapabilityMismatchfor a bad or missing entry.hal_joint_states_topic(description, *, mode, hal_node_name, override=None) -> str | None(resolver.pyL141) — TheJointStatetopic the deploy runtime's Python nodes (world_state ingest + the runner'sROSPublishingHALcache) read.override(DeployRuntime.joint_states_topic) wins; else, onmode="real"with ahal.realclass exposing everyRosControlDrivablemember (checked on the class, not constructed — same membership_attach_ros_control_transportuses),/<hal_node_name>/joint_states, the HAL's rate-limited republish instead of the broadcaster's 0.5-1 kHz/joint_statesthat starves an in-process VLA of the GIL;None(=/joint_states) for serial/interbotix/sim HALs. Called bydeploy_e2e.launch.py, which forwards the result as the runtime node'sjoint_states_topicparam.urdf_joint_names(description, urdf_xml) -> list[str](resolver.pyL196) — The URDF joint each manifest joint's state drives (itsnamewhen the URDF declares it, else itssim_joint_name, else""), parallel todescription.joints;[]when every name already agrees. A sim HAL publishes joint states under the logical names, so a URDF that names its joints differently (the OpenArm's vendored one) left every moving link off/tfand every tf2 consumer (vision attachment leg TCP / attach link, octomap payload clearing) blind.deploy_e2e.launch.pypasses the result to the sim HAL asurdf_joint_namesand remapsrobot_state_publisheronto the HAL's~/urdf_joint_states. RaisesROSConfigErroron unparseable XML._import_object(path: str) -> object[private] — Resolves a"module.path:Attribute"import string; raisesROSConfigErrorif malformed/missing. Reuse watch: the canonical entrypoint importer — don't hand-rollimportlibin HAL callers.joint_actuator_effort(data, dof) -> float(L142) — The one definition of sim/joint_states.effort: MuJoCoqfrc_actuatorat the joint's DOF (gear/transmission applied, all actuators on the DOF summed, gravity load included). Used byMujocoArmHALandSimAttachedHAL.class MujocoArmHAL— Generic MuJoCo-backed HAL adapter for position-controlled arms (and, via the_per_step_updatehook, torque-controlled humanoids like the H1). (L158)read_images() -> dict[str, NDArray](L515) — Renders the manifest's RGBSensorSpecs off the live MJCF, keyed by sensor name, each at that sensor's ownintrinsicsresolution (one cachedmujoco.Rendererper resolution). Skips a missing camera / render error with a one-shot warning; returns{}when disconnected or no RGB sensors.__init__(description, *, mjcf_path, joint_qpos_addr, actuator_index, joint_qvel_addr=None, grippers=(), keyframe_index=None, seed_ctrl_from_qpos=False, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)— Init only; MJCF loads atconnect().joint_qvel_addrdefaults tojoint_qpos_addr, overridden by humanoid HALs (G1/H1) whose floating base shifts qvel by 1.grippersis aSimGripperDescriptionsequence — one for single-arm robots, two for bimanual (Aloha, OpenArm). (L215)_per_step_update(targets) -> None— Hook run before everymj_step; default no-op, overridden by torque-mode subclasses (H1MujocoHAL) to recompute actuator torque fromqpos/qveleach step.connect() -> None(L302) — Loads the MJCF and preparesMjData; first splices in manifest RGB and depth cameras missing from the MJCF via_camera_rig.rig_cameras_into_mjcf(bare-arm twins like so100/so101). Idempotent.disconnect() -> None(L451) — Release the MuJoCo model (idempotent); resets the slot-group stager, restarting tick numbering (also after anestop).mujoco_handles() -> tuple[Any, Any] | None(L472) — Exposes the live MuJoCo(model, data)handles for the bare-twin arm, mirroringSimAttachedHAL.mujoco_handlessoSimSensorBridge's offscreen cinecam can render a 3rd-person view;Noneuntil connected.read_state() -> JointState(L608) — Joint state in description-joint order, read live fromMjData— never raisesROSPerceptionStale(the data is always current); a stale gap logs a one-shot warning instead of raising. Staleness is instead policed by the subscription HALs (ros_control/aloha), not here.effortisjoint_actuator_effortper joint (grippers at their leader DOF).send_action(action: Action) -> None(L724) — Forward last waypoint to MuJoCo and step. Stamps_last_action_nsso the idle stepper yields to a recent command. ADR-0102 slot groups, every twin in the hierarchy (ALOHA, Franka, UR5e/UR10e, Rizon4, SO-100, G1, H1, OpenArm, Anvil OpenArm v2): atick_group_size > 1action is staged in aSlotGroupStagerand, once the tick's last slot lands, composed by name (compose_slot_group_action) into one full-dofJOINT_POSITIONstep, so an arm/gripper never moves on a new chunk while the rest holds a stale one; a slot at or belowlast_committed_tickof the same runner session, or of a superseded session, is refused as a replay (ROSRuntimeError,TickWatermark). An ungrouped ticked action goes throughSlotGroupStager.admit/commit_tick, so it shares the watermark. Before this lived in the base only the OpenArm twin did it and the others refused every gripper slot (audit 2026-09-24).discard_staged_slots() -> int(L710) —SlotGroupStager.discard(watermark kept); the twin is latch-only (never receivesestop()), so the HAL lifecycle node calls this on every/openral/estoplatch and/openral/estop_cleared. Returns the slots dropped.- (property)
last_committed_tick -> int— Tick of the last slot group applied to MuJoCo (0 = none); the HAL lifecycle node acknowledges a grouped tick on/openral/action_appliedonly once it equals the tick. (L697) - (property)
last_committed_session -> int—runner_session_idoflast_committed_tick(0 = none / session-less); the ack also requires it to match. (L706) - (property)
last_applied_action -> Action | None(L686) — The command last applied to MuJoCo: the composed full-dof action for a slot group, the action itself otherwise; read from theSlotGroupStager(SlotGroupStager.last_applied_action), so it moves in step withlast_committed_tick. Fed to the grasp trigger by the lifecycle node. sim_time_ns() -> int | None(L484) — Bare-twin MuJoCo elapsed time in ns from liveMjData.time;Nonebefore connect/after disconnect/e-stop. The/clockseam for OpenArm/SO-100/SO-101 deploy-sim graphs, matchingSimAttachedHAL.sim_time_ns()._resolve_time_base() -> None[private] — Called byconnect(issue #358): (L379) with no pinnedsettle_stepsand a declareddescription.control_rate_hz, derives the physics steps per action viaopenral_core.physics_steps_per_tick(shrinkingopt.timestepso the period is a whole number of steps, logged ashal.time_base.timestep_shrunk), holds it withcheck_time_base, sets_step_while_active = Falseso the bridge's wall-time idle stepper yields to an active skill, and logssim.time_baseonce. A pinned count is honoured and logged ashal.time_base.pinned; a manifest without a rate keeps the legacy single step (hal.time_base.unresolved) — neither is timing-faithful and both stay wall-paced.- (property)
sim_dt_per_tick_s -> float | None— Sim seconds onesend_actionadvances once held to the control rate; (L443)Nonebefore connect, with a pinnedsettle_steps, or without a declared rate. clock_authority() -> ClockAuthority(L501) — ReturnClockAuthority.simulation("mujoco", timestep_s=<sim_dt_per_tick_s, else model.opt.timestep>)while connected, otherwiseClockAuthority.host_wall().idle_step(wall_dt_s=None) -> bool(L897) — Sim-only HOLD stepper that keeps cameras/joint-state live while idle; leavesctrlat its last commanded pose and, withwall_dt_s, advances that wall-time slice (capped by_IDLE_STEP_CAP). ReturnsFalseafter disconnect/e-stop, so it can never drive an e-stopped robot.- (property)
last_action_ns -> int(L888) —time.monotonic_ns()of the lastsend_action(0if never actuated). Read bySimSensorBridge.should_idle_stepto yield the idle stepper to a recently-commanded skill; mirrorsSimAttachedHAL.last_action_ns. reset_to_pose(pose: list[float]) -> None— Maintenance/test snap ofqposwithctrlre-seeded; gripper values are mapped throughSimGripperDescription.ctrl_rangeso they stay in the HAL's normalized units. Skill startup instead uses the runner's checked ramp or MoveIt. (L793)estop() -> None(L866) — Discard any half-staged slot group (keeping the committed-tick watermark), zeroctrland raiseROSEStopRequested.- (classmethod)
from_description(description, *, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5, mjcf_path_override=None) -> MujocoArmHAL— Manifest-driven constructor: builds the HAL's MJCF path, qpos/qvel/actuator maps and gripper config fromdescription.sim, removing the need for per-robot subclasses. (L1152) - (staticmethod)
_sim_kwargs_for(description, *, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5, mjcf_path_override=None) -> _MujocoArmInitKwargs— Translatesdescription.siminto the__init__kwarg dict; default 1:1 joint→qpos/actuator mapping fromdescription.joints, offset 7/6 whensim.floating_base=True. Used byfrom_descriptionand_init_from_description. (L1076) - (instance method)
_init_from_description(description, *, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5) -> None— Shared seam every thin per-robot subclass (UR5e/UR10e, Franka, ALOHA, OpenArm, Rizon4, G1, H1, SO-100) forwards to instead of hand-rollingsuper().__init__(DESC, **_sim_kwargs_for(DESC, …)). (L1205) - private:
_require_connected,_validate_action,_last_arm_targets,_apply_arm_targets,_apply_gripper_targets,_read_gripper_value,_gripper_command_to_raw,_reset_gripper_qpos,_effective_actuator_index_for - module const
_IDLE_STEP_CAP = 200(L139) — Upper bound (physics steps) on how much sim time oneidle_stepwall-time tick may advance, so an executor stall cannot fast-forward the world by seconds.
python/hal/src/openral_hal/_base.py
Shared HAL mixin — _connected flag, validation helpers, and a default disconnect.
class HALBase— Non-ABC mixin every adapter subclasses. (L53)disconnect() -> None(L71) — Flag-and-log default (guard on_connected,log.info("hal.disconnect", ...), clear the flag). Idempotent. Adapters holding a real resource (SDK handle, MuJoCo buffers, USB port) override it;AlohaHALandRosControlHALuse the default as-is.- private:
_require_connected,_require_control_mode,_validate_action_dims resolve_staleness_limit_s(description, override) -> float(L22) — A real HAL's joint-state freshness window:overridewhen notNone, elsedescription.safety.joint_state_staleness_limit_s;ROSConfigErrorwhen neither exists. Replaces every real HAL's constructor default (audit C F3): used byRosControlHAL,AlohaHALandSO100FollowerHAL._raw_floats(raw: dict[str, object], key: str, width: int) -> list[float][private] — Sharedread_statedecode:raw[key]as floats if it's a list, elsewidthzeros. Used byAlohaHAL.read_stateandRosControlHAL.read_state.
python/hal/src/openral_hal/_camera_rig.py
Generic sim camera rig — splice manifest cameras into a bare-arm MJCF for deploy sim.
rig_cameras_into_mjcf(xml: str, sensors: list[SensorSpec]) -> tuple[str, bool](L112) — For each RGB or depthSensorSpecwith asim_placementmissing fromxml(a depth sensor's camera is what the sim bridge ray-casts and renders its registered colour from), splices a<camera>into itsparent_body(wrist) or<worldbody>(overhead), plus minimal staging (visual-only floor + fill light) when the MJCF has none. Returns(xml, changed)—changed=Falsewhen no rigging is needed, so an already-composed MJCF passes through unchanged. Idempotent; raisesROSConfigErrorwhen a sensor'sparent_bodyis missing. Called byMujocoArmHAL.connect.- module const
_STAGING_FLOOR: str(L74) — Visual-only (contype=0 conaffinity=0) ground-plane<geom>XML fragment spliced in by_ensure_stagingso a bare-arm deploy twin's cameras have a surface to see.
python/hal/src/openral_hal/_real_description.py
Internal helper to derive a real-hardware RobotDescription from a sim baseline.
make_real_description(base, *, sdk_kind) -> RobotDescription—model_copy(update={"sdk_kind": sdk_kind}); thehalentrypoints (hal.sim/hal.real) are inherited from base. (L48)
python/hal/src/openral_hal/_sensor_wiring.py
Resolve sensor catalog ids into SensorSpec / SensorBundle and attach them to a RobotDescription copy — the shared helper behind every per-robot *_with_sensors(...) factory (franka_panda_with_sensors, ur5e_with_sensors, ur10e_with_sensors, so100_with_sensors).
SensorRequest = str | tuple[str, Mapping[str, Any]](L23) — Either a bare catalog id ("intel/realsense_d435") or an(id, kwargs)pair forwarding kwargs to the catalog factory.with_sensors(description, requests, *, default_parent_frame=None) -> RobotDescription(L28) — Deep-copiesdescriptionand appends catalog-resolved sensors/bundles (viaopenral_sensors.CATALOG.build); each request'sparent_framedefaults todefault_parent_frame or description.base_frame. RaisesKeyErrorfor an unknown catalog id.
python/hal/src/openral_hal/franka_panda.py
HAL adapter for the Franka Emika Panda 7-DoF arm (sim, MuJoCo).
class FrankaPandaHAL(MujocoArmHAL)— Franka Panda HAL (MuJoCo-backed). Thin manifest-driven wrapper aroundMujocoArmHAL;__init__forwards toself._init_from_description(FRANKA_PANDA_DESCRIPTION, …). (L286)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L318)_panda_joint_specs() -> list[JointSpec](L123)- module const
_PANDA_ARM_JOINT_NAMES: list[str](L53) — 7 arm joint names, upstreampanda_jointNnaming. - module const
_PANDA_GRIPPER_JOINT_NAME = "panda_gripper"(L63) — synthetic 1-DoF gripper channel, normalized like SO-100's. - module const
_PANDA_JOINT_NAMES: list[str](L65) — arm + gripper, full manifest joint order. - module const
_PANDA_SIM_JOINT_NAMES: dict[str, str](L75) — manifest joint name → native MJCF joint name, used bySimAttachedHAL.read_state's name resolution. - module const
_PANDA_POSITION_LIMITS: dict[str, tuple[float, float]](L88) — per-joint position limits (Franka FR3/Panda data sheet, rad). - module const
_PANDA_VELOCITY_LIMITS: dict[str, float](L99) — per-joint velocity limits (rad/s). - module const
_PANDA_EFFORT_LIMITS: dict[str, float](L110) — per-joint torque limits (Nm). - const
FRANKA_PANDA_DESCRIPTION = RobotDescription(...)(L177) — sim baseline;sdk_kind="open",hal.sim=None(build_hal derivesMujocoArmHALfrom the manifest) +hal.real="openral_hal.franka_panda_real:FrankaPandaRealHAL"; an in-code mirror ofrobots/franka_panda/robot.yaml, which is the runtime source of truth. All MuJoCo wiring (MJCF URI, joint→qpos/actuator maps, gripper config) lives inFRANKA_PANDA_DESCRIPTION.sim. The real-HW companionFRANKA_PANDA_REAL_DESCRIPTIONlives infranka_panda_real.py. franka_panda_with_sensors(catalog_ids=None) -> RobotDescription— Copy ofFRANKA_PANDA_DESCRIPTIONwith catalog sensors attached;Nonedefaults to the wrist-mounted RealSense D435i reference loadout. (L257)
python/hal/src/openral_hal/franka_panda_real.py
Real-hardware HAL adapter for the Franka Emika Panda over the FCI.
- module const
_DEFAULT_FRANKA_CONTROLLER: str = "franka_arm_controller"(L58) - module const
_DEFAULT_FRANKA_JOINT_STATE_TOPIC: str = "/joint_states"(L63) - module const
_DEFAULT_FRANKA_ERROR_RECOVERY: str = "/error_recovery"(L70) — thefranka_msgs/action/ErrorRecoveryname recorded for the operator; never called byestop(). class FrankaPandaRealHAL(RosControlHAL)— Production adapter for a physical Panda overfranka_ros2/ FCI. Takesdescription=(loaded manifest;None→FRANKA_PANDA_REAL_DESCRIPTION). SubclassesRosControlHAL(the UR shape) so it is structurallyRosControlDrivableand the lifecycle node attaches the productionRosControlTransportunderhal_mode:=real; the earlier composed wrapper forwarded only the five HAL Protocol methods, exposed none of the drivable surface, and was therefore never wired — a real Panda deploy published every command into_default_publish. (L89)__init__(*, fci_ip='172.16.0.2', controller_name='franka_arm_controller', joint_state_topic='/joint_states', command_topic=None, error_recovery_action='/error_recovery', publish_fn=None, state_fn=None, staleness_limit_s=None, description=None)(L165) —staleness_limit_s=Nonereads the manifest'ssafety.joint_state_staleness_limit_s.estop_recovery = RESTART_REQUIRED—stopRobot()ends the FCI session; the reflex is cleared and the FCI re-armed by the operator.fci_ip -> str[@property] (L199)error_recovery_action -> str[@property] —franka_ros2'sfranka_msgs/action/ErrorRecoverythe operator runs before re-arming; recorded, never called byestop()(it clears a reflex — a recovery, not a stop). (L204)connect() -> None— Logs the FCI target, then the baseconnect. (L210)estop()— Inherited, not overridden: deactivatingfranka_arm_controllerthroughcontroller_manageris the stop —franka_hardware'son_deactivatecallslibfranka'sstopRobot(). The earlier override published a dict to/error_recovery/goal, an undeclared topic the transport refused, so it was a silent no-op; it is gone.- Inherits
description(=FRANKA_PANDA_REAL_DESCRIPTION),controller_name,disconnect,read_state,send_action,command_bindings,attach_transport,attach_controller_stop,reset_estop(refuses:RESTART_REQUIRED) fromRosControlHAL. - const
FRANKA_PANDA_REAL_DESCRIPTION = make_real_description(FRANKA_PANDA_DESCRIPTION, sdk_kind="closed_with_api")(L83) — inherits the sharedhal; whatrobots/franka_panda/robot.yamlmirrors.
python/hal/src/openral_hal/sawyer_real.py
Real-hardware HAL adapter for the Rethink Sawyer 7-DoF arm.
- module const
_SAWYER_JOINT_NAMES: tuple[str, ...](L66) — 7 arm joint names. - module const
_SAWYER_POSITION_LIMITS: dict[str, tuple[float, float]](L79) - module const
_SAWYER_VELOCITY_LIMITS: dict[str, float](L89) - module const
_SAWYER_EFFORT_LIMITS: dict[str, float](L99) - module const
_DEFAULT_SAWYER_CONTROLLER: str = "sawyer_arm_controller"(L223) - module const
_DEFAULT_SAWYER_JOINT_STATE_TOPIC: str = "/robot/joint_states"(L228) - module const
_DEFAULT_SAWYER_ESTOP_TOPIC: str = "/robot/set_super_stop"(L237) class SawyerRealHAL(RosControlHAL)— Production adapter for a physical Sawyer overintera_sdk/sawyer_robot. Takesdescription=(loaded manifest;None→SAWYER_REAL_DESCRIPTION). SubclassesRosControlHALfor the same reason asFrankaPandaRealHAL: the composed wrapper it replaced was notRosControlDrivable, so the lifecycle node never attached the production transport to it. (L243)__init__(*, hostname='sawyer.local', controller_name='sawyer_arm_controller', joint_state_topic='/robot/joint_states', command_topic=None, estop_topic='/robot/set_super_stop', publish_fn=None, state_fn=None, staleness_limit_s=None, description=None)(L317) —staleness_limit_s=Nonereads the manifest's window.hostname -> str[@property] (L351)connect() -> None(L355)estop_recovery = RESTART_REQUIRED— the super stop is cleared by/robot/set_super_reset+ re-enable, an operator step.- (property)
estop_topic -> str—/robot/set_super_stop, intera's super-stop topic. (L370) vendor_stop_topics() -> list[str]—[estop_topic], declared so the transport creates itsstd_msgs/Emptypublisher at wire-up. (L376)_vendor_stop(seam) -> str— After the base deactivatedsawyer_arm_controller, publishes intera's super stop (RobotEnable.stop()'s topic: "Simulate an e-stop button being pressed. Robot must be reset to clear the stopped state"). The topic carries no service ack; the acknowledged half is the controller deactivation, and the HIL gate reads/robot/state.stopped.- Inherits
description(=SAWYER_REAL_DESCRIPTION),controller_name,disconnect,read_state,send_action,command_bindings,attach_transport,attach_controller_stop,estop,reset_estop(refuses:RESTART_REQUIRED) fromRosControlHAL. _sawyer_joint_specs() -> list[JointSpec](L110)- const
SAWYER_DESCRIPTION = RobotDescription(...)(L153) — sim baseline;sdk_kind="open",hal.sim=None(no MuJoCo HAL adapter today) +hal.real="openral_hal.sawyer_real:SawyerRealHAL". - const
SAWYER_REAL_DESCRIPTION = make_real_description(SAWYER_DESCRIPTION, sdk_kind="closed_with_api")(L213) — inherits the sharedhal; whatrobots/sawyer/robot.yamlmirrors.
python/hal/src/openral_hal/panda_mobile.py
In-process digital-twin HAL for the panda_mobile embodiment (Franka 7-DoF arm on a holonomic 3-DoF base). Built by build_hal for the manifest-driven ManifestHALLifecycleNode and by tests; ROS node entrypoint is the generic packages/openral_hal_node/.
- const
PANDA_MOBILE_BASE_JOINT_NAMES: list[str]— Base joints[base_x, base_y, base_yaw], derived fromPANDA_MOBILE_DESCRIPTION.base_joints(not hardcoded). (L84) - const
PANDA_MOBILE_JOINT_NAMES: list[str]— Full 11-DoF order: base (3) + arm (7, role-derived) + gripper (1, role-derived) — all from the description. (L100) - const
PANDA_MOBILE_DESCRIPTION: RobotDescription— Canonical RobotDescription, loaded fromrobots/panda_mobile/robot.yamlat module import. Single source of truth for joint metadata +sim_joint_nameoverrides; the arm/base/gripper name constants above derive from it viaJointSpec.role. (L77) class PandaMobileHAL— In-process digital-twin HAL,PandaMobileHAL(*, initial_pose=None, dt_s=0.05, description=None).descriptionis the loaded manifest (build_halthreads it, sopanda_mobile_vslamreports its own sensors);Nonefalls back torobots/panda_mobile/robot.yaml. Joint layout (base frombase_joints, arm/gripper byrole) is read per instance from it. RoutesBODY_TWIST→ planar Euler integration of (vx, vy, wz); routesJOINT_POSITION→ 7-vec arm targets or 11-vec base+arm+gripper targets. (L121)connect() -> None(L198) — Idempotent; flips the connected flag.disconnect() -> None(L202) — Idempotent; clears the connected flag.read_state() -> JointState(L206) — Fresh 10-DoF snapshot; raisesROSConfigErrorif called beforeconnect().send_action(action: Action) -> None(L219) — Applies the action to in-memory state; acceptsJOINT_POSITION/BODY_TWIST/CARTESIAN_DELTA/GRIPPER_POSITION, each reading its matchingActionfield. No-ops while e-stop is latched. RaisesROSConfigErrorfor an unsupported mode or a missing/mis-shaped payload.estop() -> None(L276) — Latch the e-stop flag; subsequentsend_actioncalls no-op untilreset_estop.reset_estop() -> None(L287) — Clear the e-stop latch; caller asserts the cause has been resolved.- (property)
estop_latched -> bool(L292) - (property)
base_pose -> tuple[float, float, float](L297) — Current base(x, y, yaw)for tests / odom publishers. - (property)
base_twist -> tuple[float, ...](L302) — Last commanded base body twist(vx, vy, vz, wx, wy, wz); zeroed once a non-BODY_TWIST action is sent, and reads zero once no BODY_TWIST arrived for_TWIST_STALE_PERIODS(2) control periods — the base only moves on a command, so a stopped stream is a still base. - module const
_PANDA_MOBILE_ARM_JOINT_NAMES(L88) — the 7 Franka arm joint names within the 11-DoF panda_mobile order. - module const
_PANDA_MOBILE_GRIPPER_JOINT_NAME(L96) — the 1-DoF gripper joint name. - module const
_PLANAR_TWIST_EPS(L113) — tolerance for detecting a non-planarBODY_TWISTcomponent. - (removed: the
base_sim_joint_namesre-export wrapper — callers now importopenral_core.extract_base_sim_joint_namesdirectly.)
python/hal/src/openral_hal/depth_cloud.py
Reusable, robot-agnostic depth-camera → sensor_msgs/PointCloud2 plumbing for deploy-sim HAL nodes (octomap_server source → kernel world-collision check). Pure SensorSpec adapters + the ROS msg builder; the ray-cast synth lives in openral_sim.backends.depth_camera.
- module const _OPTICAL_IN_MJCAM (L37) — REP-103 optical frame (x right, y down, z forward) expressed in the MuJoCo camera frame, as a diagonal flip-y/z matrix.
- module const _MIN_EYE_DISTANCE_M = 1e-6 (L40) — degenerate eye→lookat distance below which azimuth/elevation are undefined.
- module const _VIEWER_LOOKAT_LIFT_M = 0.7 (L45) — deploy-viewer orbit-pivot lift off the floor onto the robot body.
- module const _VIEWER_PULLBACK_M = 2.0 (L46) — deploy-viewer eye pullback so cluttered mobile-manip scenes show the whole robot.
- module const _VIEWER_CAMERA_PREFS (L251) — default prefer substrings for preferred_viewer_camera_id ("agentview", "top", "frontview", "front").
- is_depth_sensor(spec) -> bool — True when spec.modality in ("depth", "point_cloud") and it carries pinhole intrinsics (required to back-project). (L52)
- module const BODY_TO_OPTICAL_QUAT_XYZW = (-0.5, 0.5, -0.5, 0.5) (L64) — REP-103 body frame → its optical child, rpy (-π/2, 0, -π/2).
- depth_optical_frame_id(spec) -> str (L67) — The optical frame a depth sensor's sim raster, cloud and registered colour are stamped in: spec.frame_id when it is already an *_optical_frame, else its REP-103 child <frame_id>_optical_frame (a body-convention camera mount like the ZED's zed_camera_link). Stamping optical data with the body frame rotated every consumer's geometry.
- mjcf_camera_name(spec) -> str — Resolves the backing MJCF <camera> name: spec.metadata["mjcf_camera"] if set (the sim camera name can differ from the ROS sensor name), else spec.name. (L89)
- robot_self_body_ids(model, sim_joint_names) -> frozenset[int] — Every MJCF body sharing a first-_-token prefix with one of the robot's sim_joint_names (mobilebase0/robot0/gripper0). Passed as synthesize_depth_image(exclude_body_ids=…) so the depth raster self-filters the robot out of its own world map. (L140)
- depth_synth_kwargs(spec, *, max_range_default, render_size=None) -> dict — Maps a depth SensorSpec to depth-synth kwargs (intrinsics + min/max_range_m, falling back to max_range_default); with render_size, intrinsics are rescaled via openral_core.scale_intrinsics_to to match the render resolution. (L102)
- resolve_base_body_name(model, *, description=None) -> str | None — Resolves the MJCF body backing the robot's base_frame (first base joint's prefix + _base, else mobilebase0_base/base/robot0_base/base_link); None if none exist. Backs the depth self-filter anchor and viewer fallback — not the TF/extrinsic parent, which is resolve_base_frame_body_name. (L172)
- resolve_base_frame_body_name(model, *, description=None) -> str | None — Resolves the MJCF body whose pose base_frame actually carries on /tf (ADR-0095): tries the base joint's _support body (robosuite's arm-mount plate) before delegating to resolve_base_body_name. They differ only on robosuite/RoboCasa mobile manipulators, whose chassis root sits well below the arm mount MobileBaseBridge publishes as base_link; fixed-base arms resolve identically either way. (L208)
- preferred_viewer_camera_id(model, *, prefer=("agentview","top","frontview","front")) -> int — Picks the MJCF camera the viewer should open from: first camera name matching a prefer substring, else the first declared camera, else -1 if none. Consumed by initial_viewer_camera. (L254)
- initial_viewer_camera(*, model, data, description=None) -> tuple[tuple[float,float,float], float, float, float] — Opening free-camera pose (lookat, distance, azimuth_deg, elevation_deg) for the viewer (always mjCAMERA_FREE, so mouse orbit/zoom stay live). Eye placed at the preferred_viewer_camera_id camera with orbit pivot on the robot base when one exists, else delegates to base_aligned_free_camera. (L407)
- apply_robosuite_visual_geomgroups(opt, model) -> bool — For a robosuite/RoboCasa model, hides collision shells (geomgroup 0) and shows textured visual geoms (group 1) so mujoco.viewer renders textures instead of collision boxes. Gated on a robosuite signature, not geom counts (dm_control/gym-aloha put visuals in group 0). Used by sim run --view. (L294)
- base_aligned_free_camera(*, model, data, base_body_name=None, azimuth_offset_deg=135.0, elevation_deg=-25.0, distance_scale=2.0, max_distance_m=3.5) -> tuple[tuple[float,float,float], float, float, float] — Fallback free-camera framing for camera-less models: centres on the robot base, offset by its world yaw, at distance_scale × model.stat.extent capped at max_distance_m. Falls back to model.stat.center with no yaw when base_body_name is absent. (L325)
- camera_optical_tf_to_base(*, model, data, camera_name, base_body_name) -> tuple[tuple[float,float,float], tuple[float,float,float,float]] — Live (translation_xyz, quat_xyzw) of the camera optical frame (REP-103) expressed in the base body, for broadcasting base_frame → <camera>_optical_frame. Raises ROSConfigError if camera/body absent. (L461)
- pointcloud2_from_points_xyz(points, *, frame_id, stamp=None, rgb=None) -> PointCloud2 — Packs an (N, 3) float32 array into an unordered XYZ-float32 sensor_msgs/PointCloud2, the layout octomap_server's cloud_in expects; with rgb each point also carries the PCL rgb field. (L513)
- colours_from_frame(points, rgb, k, optical_in_base) -> NDArray[np.uint8] — Colours base-frame points by projecting them into a registered RGB frame; behind the camera or outside the image is grey. (L564)
- depth_image_from_grid(depth, *, frame_id, stamp=None) -> Image — Packs an (H, W) float32 metric-depth raster into a 32FC1 sensor_msgs/Image (0.0 = no measurement) for nvblox's projective depth integrator. (L595)
- points_from_depth_grid(depth, *, fx, fy, cx, cy, clearing=None, max_range_m=None) -> NDArray[np.float32] — Back-projects an (H, W) metric-depth raster into an (N, 3) optical-frame cloud, dropping 0.0 pixels. clearing re-adds the self-filter's clearing rays at max_range_m so OctoMap can clear cells behind the robot's silhouette; without it those cells can never clear. Raises ROSConfigError on a shape mismatch or a marked mask with no max_range_m. (L632)
- depth_grid_from_image(msg) -> NDArray[np.float64] — Inverse of depth_image_from_grid: decodes a driver's depth Image into an (H, W) metre raster, accepting 32FC1 (metres) and 16UC1 (millimetres, rescaled). Raises ROSConfigError on an unsupported encoding or a length mismatch. Used by VisionAttachmentBridge to read real wrist-camera depth. (L740)
- camera_info_from_intrinsics(*, width, height, fx, fy, cx, cy, frame_id, stamp=None) -> CameraInfo — Builds a pinhole sensor_msgs/CameraInfo for a synthesised depth image — K=[fx,0,cx;0,fy,cy;0,0,1], identity R, P mirroring K (no baseline), zero plumb_bob distortion (MuJoCo ray-cast has none). Callers pass the stride-scaled intrinsics so the model matches the rasterised image. (L787)
- intrinsics_from_camera_info(msg) -> IntrinsicsPinhole — Inverse of camera_info_from_intrinsics, duck-typed: fx=k[0], fy=k[4], cx=k[2], cy=k[5] at the message's width/height. How a consumer projects through the driver's live calibration instead of a manifest's nominal intrinsics (OpenArm ZED Mini: fx 1498.18 published vs 960 declared, Thor cell 2026-10-02). Raises ROSConfigError on a missing / non-9 k, a non-positive focal length or a zero resolution (the all-zero uncalibrated default). Used by segmenter_node.projection_geometry. (L836)
- resample_mask_nearest(mask, shape) -> NDArray[np.bool_] — Nearest-neighbour resample of a boolean mask on pixel centres (floor((r + 0.5) * H_src / H_dst)), to bring an RGB-resolution mask onto the same camera's depth raster before backproject_masked_depth. Raises ROSConfigError on a non-2-D mask, non-positive shape, or aspect ratios differing by >1 % (a different crop, not a different resolution). (L885)
python/hal/src/openral_hal/aloha.py
HAL adapter for the Trossen ALOHA bimanual setup, plus the MuJoCo digital twin.
class AlohaHAL(HALBase)— Real-hardware adapter for the 14-DoF ALOHA over the Interbotix XS SDK. Takesdescription=(loaded manifest;None→ALOHA_REAL_DESCRIPTION). Stays onHALBase, notRosControlHAL: the driver owns the bus, likeSO100FollowerHAL. Its command topics are ros2_control names a real ALOHA does not expose, sosend_actionreports success and moves nothing — issue #250 holds thexs_sdkwire contract and the on-rig checks needed before changing the defaults. Its e-stop, by contrast, targets what a real ALOHA does expose:estop_recovery = RESTART_REQUIRED, andestop()cuts torque on every arm namespace through an attachedInterbotixStopSeam. (L411)__init__(*, left_arm_controller=..., right_arm_controller=..., left_gripper_controller=..., right_gripper_controller=..., joint_state_topic='/joint_states', arm_namespaces=('follower_left', 'follower_right'), publish_fn=None, state_fn=None, staleness_limit_s=None, stop_timeout_s=5.0, description=None)—staleness_limit_s=Nonereads the manifest's window (0.2 s, formerly a code-only default). — The formerestop_topic(/aloha/estop, a broadcast for a watchdog node that does not exist in this repo) is gone;arm_namespacesnames thexs_sdkrobots the stop torques off. (L484)arm_namespaces() -> list[str]— Thexs_sdkrobot namespaces the stop torques off; theInterbotixStoppablesurface the lifecycle node reflects on. (L522)attach_torque_stop(seam) -> None— Bind theInterbotixStopSeamafter construction (build_halruns before any ROS node exists); rejects a non-seam withROSConfigError. (L526)- (property)
last_stop_report -> DownstreamStopReport | None— TheDownstreamStopReportingsurface the lifecycle heartbeat and FATAL log read. (L540) connect() -> None(L546)read_state() -> JointState(L563)send_action(action) -> None— Splits the 14-D action 4-ways across per-arm + per-gripper controllers. (L589)estop() -> None— Drop the connection flag first, thentorque_enable(cmd_type='group', name='all', enable=false)on every arm namespace (each attempted even if an earlier one refused), record aDownstreamStopReport(stoppedonly when every arm acknowledged;controller_statesper armtorque_off/torque_unknown), raiseROSEStopRequested. Torque off leaves the ViperX arms limp (no brakes) — the same outcome as the rig's hardware e-stop and the only stopxs_sdkoffers. (L653)- private:
_require_connected,_cut_torque class InterbotixStopSeam(Protocol)—torque_enable(robot_name, *, group, enable, timeout_s) -> TriggerReport; productionInterbotixXSTransport, unit laneSimTorqueSeam. (L383)torque_enable(robot_name, *, group, enable, timeout_s) -> TriggerReport—/<robot_name>/torque_enablewithcmd_type='group';successis the call completing. (L391)class InterbotixStoppable(Protocol)—arm_namespaces()+attach_torque_stop(seam); what the lifecycle node's_attach_interbotix_transportreflects on. (L399)arm_namespaces() -> list[str](L402)attach_torque_stop(seam) -> None(L406)class AlohaMujocoHAL(MujocoArmHAL)— MuJoCo digital twin for the 14-DoF bimanual ALOHA; thin manifest-driven wrapper aroundMujocoArmHAL(bimanual amendment). All wiring lives inALOHA_DESCRIPTION.sim:gym_aloha:bimanual_viperx_transfer_cubeURI, explicitjoint_qpos_addr/actuator_index(left arm 0-5, left gripper 6, right arm 8-13, right gripper 14 — skipping the negative-finger slots), two grippers on a normalized[0, 1]channel (0 closed, 1 open;NORMALISEDwrites,AFFINE_LOW_HIGHreads, mapped onto the finger range[0.021, 0.057]m) withmirror_actuator_index(positive finger + negative finger),keyframe_index: 0(seeds the fingers inside that range). (L732)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)— Forwards toself._init_from_description(ALOHA_DESCRIPTION, …). (L770)_aloha_joint_specs() -> list[JointSpec](L171)_default_publish(topic, msg) -> None(L720)- module const
_ALOHA_LEFT_ARM_JOINTS: tuple[str, ...](L116) — left-arm joint names, ViperX 300 order. - module const
_ALOHA_LEFT_GRIPPER_JOINT: str(L124) — left gripper joint name. - module const
_ALOHA_RIGHT_ARM_JOINTS: tuple[str, ...](L125) — right-arm joint names. - module const
_ALOHA_RIGHT_GRIPPER_JOINT: str(L133) — right gripper joint name. - module const
_ALOHA_JOINT_NAMES: tuple[str, ...](L135) — full 14-DoF joint order: left arm + left gripper + right arm + right gripper. - module const
_PI: float = 3.14159(L147) — truncated π matching the YAML manifest's string form, used by_ALOHA_ARM_POSITION_LIMITSso the manifest-vs-HAL drift guard stays byte-equal. - module const
_ALOHA_ARM_POSITION_LIMITS: dict[str, tuple[float, float]](L148) — per-joint-group position limits from the ViperX 300 data sheet. - module const
_ALOHA_GRIPPER_POSITION_LIMITS: tuple[float, float](L160) - module const
_ALOHA_ARM_AXIS: dict[str, tuple[float, float, float]](L161) — per-joint-group rotation axis. - module const
_DEFAULT_LEFT_ARM_CONTROLLER: str(L360) —AlohaHAL's default left-armxs_sdkcontroller name. - module const
_DEFAULT_RIGHT_ARM_CONTROLLER: str(L361) — default right-arm controller name. - module const
_DEFAULT_LEFT_GRIPPER_CONTROLLER: str(L362) — default left-gripper controller name. - module const
_DEFAULT_RIGHT_GRIPPER_CONTROLLER: str(L363) — default right-gripper controller name, matching the udev-pinned CAN dev-id convention. - module const
_DEFAULT_ALOHA_JOINT_STATE_TOPIC: str(L367) — default/joint_statestopic. - module const
_DEFAULT_ALOHA_ARM_NAMESPACES: tuple[str, str](L375) — thexs_sdkrobot namespacesestoptorques off (follower_left,follower_right). - module const
_TORQUE_GROUP_ALL = "all"(L376) — theTorqueEnablegroup name covering every joint of one arm. - const
ALOHA_DESCRIPTION = RobotDescription(...)(L215) — sim baseline;sdk_kind="open",hal.sim="openral_hal.aloha:AlohaMujocoHAL"+hal.real="openral_hal.aloha:AlohaHAL". - const
ALOHA_REAL_DESCRIPTION = make_real_description(ALOHA_DESCRIPTION, sdk_kind="closed_with_api")(L348) — inherits the sharedhal; whatrobots/aloha_bimanual/robot.yamlmirrors.
python/hal/src/openral_hal/ur.py
HAL adapters for the Universal Robots UR5e and UR10e arms (sim, MuJoCo).
class UR5eHAL(MujocoArmHAL)— UR5e HAL (MuJoCo-backed). Thin manifest-driven wrapper;__init__forwards toself._init_from_description(UR5e_DESCRIPTION, …). (L345)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L371)class UR10eHAL(MujocoArmHAL)— UR10e HAL (MuJoCo-backed). Same shape asUR5eHAL. (L389)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L401)ur5e_with_sensors(catalog_ids=None) -> RobotDescription(L289)ur10e_with_sensors(catalog_ids=None) -> RobotDescription(L315)_ur_joint_specs(velocity_limits, effort_limits) -> list[JointSpec](L120)- module const
_UR_JOINT_NAMES: list[str](L59) — 6 arm joint names, shared by UR5e/UR10e. - module const
_UR5E_POSITION_LIMITS: dict[str, tuple[float, float]](L70) — shared by UR5e and UR10e (same joint range family). - module const
_UR5E_VELOCITY_LIMITS: dict[str, float](L79) - module const
_UR5E_EFFORT_LIMITS: dict[str, float](L89) - module const
_UR10E_VELOCITY_LIMITS: dict[str, float](L100) - module const
_UR10E_EFFORT_LIMITS: dict[str, float](L110) - const
UR5e_DESCRIPTION = RobotDescription(...)(L158) — sim manifest; all MuJoCo wiring lives inUR5e_DESCRIPTION.sim. - const
UR10e_DESCRIPTION = RobotDescription(...)(L223) — sim manifest; all MuJoCo wiring lives inUR10e_DESCRIPTION.sim.
python/hal/src/openral_hal/ur_real.py
Real-hardware HAL adapters for UR5e / UR10e via ros2_control + ur_robot_driver (URCap / RTDE).
class UR5eRealHAL(_URRealHAL)— Real UR5e viaur_robot_driver. Takesdescription=(loaded manifest;None→UR5e_REAL_DESCRIPTION). (L197)class UR10eRealHAL(_URRealHAL)— Real UR10e viaur_robot_driver. Takesdescription=(loaded manifest;None→UR10e_REAL_DESCRIPTION). (L254)class _URRealHAL(RosControlHAL)— Shared real-HW base (controller / topic defaults +deadman_topic+dashboard_stop_service).estop_recovery = RESTART_REQUIRED. (L87)vendor_stop_services() -> list[str]—[dashboard_stop_service](default/dashboard_client/stop). (L171)_vendor_stop(seam) -> str— After the base deactivatedscaled_joint_trajectory_controller, calls the dashboardstop(std_srvs/Trigger), which stops theexternal_controlprogram on the pendant so the robot performs a controlled stop and the driver's control connection ends; a refused Trigger raisesROSRuntimeErrorso the report reads unacknowledged. Recovery: operator restarts the program (/dashboard_client/play+resend_robot_program), relaunches the HAL node, re-aligns. (L175)- module const
_UR_CONTROLLER_NAME: str(L60) — defaultur_robot_driverJointTrajectoryControllername. - module const
_UR_JOINT_STATE_TOPIC: str(L61) - module const
_UR_DEADMAN_TOPIC: str(L62) - module const
_UR_DASHBOARD_STOP_SERVICE: str(L69) — defaultur_robot_driverdashboardstopservice (std_srvs/Trigger). - const
UR5e_REAL_DESCRIPTION = make_real_description(UR5e_DESCRIPTION, sdk_kind="closed")(L76) — inherits the sharedhal; whatrobots/ur5e/robot.yamlmirrors. - const
UR10e_REAL_DESCRIPTION = make_real_description(UR10e_DESCRIPTION, sdk_kind="closed")(L81) — inherits the sharedhal; whatrobots/ur10e/robot.yamlmirrors.
python/hal/src/openral_hal/so100_follower.py
SO100FollowerHAL — wraps lerobot's SO-100 follower arm USB driver.
- module const
_SO100_JOINT_NAMES: list[str](L81) — canonical joint order, matching lerobot's bus motor dict order. - module const
_RESET_MAX_RAD_S = 0.5(L96) —reset_to_poseramp speed cap. - module const
_RESET_MIN_S = 1.0(L97) —reset_to_poseramp minimum duration. - module const
_RESET_MAX_S = 6.0(L98) —reset_to_poseramp maximum duration. - module const
_RESET_STEP_HZ = 30.0(L99) —reset_to_poseramp waypoint rate. - module const
_RESET_DEADBAND_RAD(L102) — below this max joint delta the arm counts as already at the target and the ramp is skipped. class SO100FollowerHAL— HAL adapter wrapping lerobot's SO-100/SO-101 follower. Takesdescription=(the loaded manifest, so an SO-101 deploy reportsso101_follower);Nonefalls back toSO100_DESCRIPTION. Declaresestop_recovery = RESTART_REQUIRED: its vendor E-stop disconnects the motor bus, so command execution requires a fresh lifecycle start and alignment. (L313)__init__(port='/dev/ttyUSB0', *, calibrate_on_connect=False, id=None, calibration_dir=None, max_relative_target=None, staleness_limit_s=None, robot=None, description=None)(L363) —staleness_limit_s=Nonereads the manifest's window.connect() -> None— Open USB serial connection. (L408)disconnect() -> None— Close USB, disable motor torque (idempotent). (L524)read_state() -> JointState— Joint state in radians. (L537)send_action(action: Action) -> None— Forward one step to the SO-100 motor bus. (L565)reset_to_pose(pose: list[float]) -> None— Explicit maintenance/test ramp from current → target (speed-capped_RESET_MAX_RAD_S, duration clamped[_RESET_MIN_S, _RESET_MAX_S],_RESET_STEP_HZwaypoints). The lifecycle node exposes it at/openral/<robot>/reset_to_pose; skill startup uses the runner's safety-kernel path. (L588)estop() -> None— Disconnect motors then raise. (L651)_require_connected(operation: str),_obs_to_positions(obs)[@staticmethod],_action_to_lerobot(action)_joint_values_to_lerobot(step) -> dict[str, float](module-level) — THE single manifest-order → lerobot{"<joint>.pos": …}unit conversion (rad→deg arm joints,[0,1]→[0,100]gripper); both_action_to_lerobotand thereset_to_poseramp route through it so a calibration/range change can never apply to one actuation path and not the other. (L285)_deg_to_rad(deg) -> float(L280)_rad_to_deg(rad) -> float(L305)- const
SO100_DESCRIPTION = RobotDescription(...)(L106) so100_with_sensors(catalog_ids=None) -> RobotDescription— Copy ofSO100_DESCRIPTIONwith catalog sensors attached;Nonedefaults to the LeRobot reference loadout (["logitech/c920"]). (L249)
python/hal/src/openral_hal/galaxea_a1.py
Real-only Galaxea A1 HAL. OpenRAL stays ROS 2 / Python 3.12; the operator's official ROS 1 Noetic SDK runs out of process behind a literal IPv4-loopback JSON-lines sidecar. No vendor source, binary, or message package is distributed.
class GalaxeaA1HAL(HALBase)(L577) — Six-axis joint-position + normalized-gripper adapter. Takesdescription=(loaded manifest;NonekeepsGALAXEA_A1_DESCRIPTION).read_state/send_actionuse a cached snapshot/latest target so network I/O stays off the hot path. Commands fail closed on stale state/status, unaccepted motor bits, non-finite values, target misalignment, or an excessive feedback-relative step.estopasks the sidecar to stop its owned ROS 1 stack and always raisesROSEStopRequested.connect() -> None(L699) — Connect to the sidecar (sends thehellohandshake with joint names/limits/timeouts/masks); requires one complete fresh snapshot. RaisesROSRuntimeErrorif already connected.disconnect() -> None(L733) — Close the sidecar transport idempotently.read_state() -> JointState(L738) — Return the latest non-stale six-joint feedback snapshot.send_action(action: Action) -> None(L750) — Queue one validated joint or normalized-gripper target; raisesROSConfigErrorfor an unsupported control mode.health() -> HALHealthReport(L805) — Cached hardware health for the lifecycle diagnostics heartbeat (sidecar address, feedback/status ages, motor status, relay state, staged/forwarded targets).estop() -> None(L827) — Stop the sidecar-owned ROS 1 stack (bounded, SIGINT then SIGKILL) and always raiseROSEStopRequested.- module const
_JOINT_NAMES(L63) — sixarm_jointNnames. - module const
_JOINT_LIMITS(L64) — per-joint(min, max)position limits, official A1 URDF. - module const
_JOINT_ORIGINS_XYZ(L72) — per-joint URDF origin translation, transcribed from the official A1 URDF. - module const
_JOINT_ORIGINS_RPY(L80) — per-joint URDF origin rotation, transcribed from the official A1 URDF. - module const
_PROTOCOL_VERSION = 1(L88) — sidecar wire-protocol version sent in thehellohandshake. - module const
_GRIPPER_STATUS_INDEX(L89) — index of the gripper's status code within the sidecar's status array. - module const
_MAX_TCP_PORT = 65536(L90) — upper bound accepted for the sidecar's loopback TCP port. - module const
_MAX_STATUS_MASK = 0xFFFFFFFF(L91) — bit-mask upper bound for a motor status/error mask. - const
GALAXEA_A1_DESCRIPTION(L94) — Real-onlyRobotDescription, mirrored byrobots/galaxea_a1/robot.yaml: official A1 URDF joint names/limits, sidecar deadlines, motor masks, 0..104 mm normalized gripper mapping, and calibrated D455 front / D405 wrist RGB observation contracts. Collision primitives are absent until their redistribution/lowering provenance is cleared. tools/galaxea_a1_ros1_sidecar.py— Python-3.8 ROS 1 process owningroscore,signal_arm/single_arm_node.launch, and the officialmobiman/jointTracker_demo_nodebinary. A sidecar-owned relay is the sole publisher to/arm_joint_command_host, gatedLOCKED → ARMING → ACTIVE(aligned to target + feedback before forwarding); a gripper command while notACTIVEis refused fail-closed. A lease timeout, malformed command, stale feedback, motor fault, disconnect, or e-stop stops the whole owned process group.tools/run_galaxea_a1_sidecar.sh— Docker launcher requiring an explicit operator-provided image and SDK path; mounts both read-only, claims only the selected serial device.--check-onlyverifies image, SDK files, cache, serial ownership, loopback port, container name, and process lock without opening the serial device or starting a container.
Galaxea A1 hardware bring-up and the LingBot-VA rSkill deploy runbook moved to robots/galaxea_a1/README.md.
python/hal/src/openral_hal/h1.py
MuJoCo digital twin for the Unitree H1 humanoid (Menagerie MJCF). Contract validator only — falls without an S0 cerebellum, so gravity must stay disabled in closed-loop tests. Unlike G1/UR/Franka/SO-100, the H1 menagerie ships torque actuators, so this HAL runs a software PD position loop every physics step.
class H1MujocoHAL(MujocoArmHAL)— Takesdescription=(loaded manifest;None→H1_DESCRIPTION). 19-DoF humanoid HAL drivingmujoco_menagerie/unitree_h1/h1.xml(legs + torso + arms, no wrists); thin manifest-driven wrapper aroundMujocoArmHAL. Overrides_apply_arm_targetsto a no-op and_per_step_updateto computetau = kp*(target - q) - kv*dqclamped toctrlrange, so the public action contract stays "position targets in radians" — mirroring howunitree_sdk2wraps torque control in a position loop on real hardware. (L329)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L370)_per_step_update(targets) -> None— Recomputes PD torque everymj_step._apply_arm_targets(targets) -> None— No-op (PD loop runs per-step instead).- module const
_H1_LEFT_LEG_JOINTS: tuple[str, ...](L80) — left-leg joint names. - module const
_H1_RIGHT_LEG_JOINTS: tuple[str, ...](L87) — right-leg joint names. - module const
_H1_TORSO_JOINTS: tuple[str, ...](L94) — torso joint name(s). - module const
_H1_LEFT_ARM_JOINTS: tuple[str, ...](L95) — left-arm joint names. - module const
_H1_RIGHT_ARM_JOINTS: tuple[str, ...](L101) — right-arm joint names. - module const
_H1_JOINT_NAMES: tuple[str, ...](L107) — full 19-DoF joint order (legs + torso + arms). - module const
_H1_POSITION_LIMITS: dict[str, tuple[float, float]](L122) — per-joint position limits. - module const
_H1_EFFORT_LIMITS: dict[str, float](L149) — per-joint actuator effort ceiling used to clamp the software PD loop's torque. - module const
_H1_VELOCITY_LIMITS_BY_GROUP: dict[str, float](L176) — per-kinematic-group velocity limits. - module const
_H1_KP_BY_GROUP: dict[str, float](L309) — per-kinematic-group PD proportional gain. - module const
_H1_KV_BY_GROUP: dict[str, float](L317) — per-kinematic-group PD derivative gain. _h1_group(joint_name) -> str— Return the kinematic group token (hip/knee/ankle/torso/shoulder/elbow) forjoint_name. (L189)_h1_parent_child(joint_name) -> tuple[str, str]— Return(parent_link, child_link)for an H1 joint. (L194)_h1_joint_specs() -> list[JointSpec]— Build the 19JointSpecs from the joint-name tuples + the per-joint limit tables. (L230)_h1_pd_gains() -> dict[str, tuple[float, float]]— Per-joint(kp, kv)for the software PD loop (kv = 0.05*kp; kp sized so a 1-rad error roughly saturates each actuator's ctrlrange). (L320)- const
H1_DESCRIPTION = RobotDescription(...)(L253) — Sim baseline;sdk_kind="open",hal.sim="openral_hal.h1:H1MujocoHAL",hal.real=None(sim-only). In-code mirror ofrobots/h1/robot.yaml(the runtime source;build_halpasses it asdescription=); drift-guarded againstrobots/h1/robot.yaml.
python/hal/src/openral_hal/flexiv_rizon4.py
MuJoCo digital twin for the Flexiv Rizon 4 — 7-DoF cobot with whole-body force sensitivity (0.1 N). Structurally identical to the UR / Franka sim HALs: position actuators, no gripper, no floating base, no PD-loop overrides — a clean MujocoArmHAL subclass.
class Rizon4MujocoHAL(MujocoArmHAL)— 7-DoF HAL drivingmujoco_menagerie/flexiv_rizon4/flexiv_rizon4.xmlviaMujocoArmHAL. Thin manifest-driven wrapper;__init__forwards toself._init_from_description(RIZON4_DESCRIPTION, …). (L180)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L211)_rizon4_joint_specs() -> list[JointSpec]— Build the 7JointSpecs from the joint-name tuple + per-joint limit tables. (L111)- module const
_RIZON4_JOINT_NAMES: tuple[str, ...](L65) — 7 arm joint names. - module const
_RIZON4_POSITION_LIMITS: dict[str, tuple[float, float]](L80) - module const
_RIZON4_VELOCITY_LIMIT: float(L94) - module const
_RIZON4_EFFORT_LIMITS: dict[str, float](L100) - const
RIZON4_DESCRIPTION = RobotDescription(...)(L132) — Sim baseline;sdk_kind="open",hal.sim=None(derivedMujocoArmHAL),hal.real=None(sim-only). In-code mirror ofrobots/rizon4/robot.yaml; drift-guarded againstrobots/rizon4/robot.yaml.
python/hal/src/openral_hal/openarm.py
MuJoCo digital twin for the Enactic OpenArm v2 bimanual humanoid arm. Fresh HALBase subclass — v2's native <position> actuators with per-class PD baked into the MJCF mean the HAL just writes target → ctrl and steps, no software PD loop needed.
class OpenArmMujocoHAL(MujocoArmHAL)(L0) — 16-DoF (7 arm + 1 gripper per side) bimanual HAL drivingenactic/openarm_mujoco/v2/openarm_v20_bimanual.xml; thin manifest-driven wrapper aroundMujocoArmHAL. All wiring (MJCF URI fetched viaensure_openarm_v2_mjcf, joint/actuator maps, gripper config) lives inOPENARM_DESCRIPTION.sim. (L474) ADR-0102 slot-group reassembly, the stale-tick guard andlast_committed_tickare inherited fromMujocoArmHAL.send_action(this class held its own copy until 2026-09-24).__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)— Forwards toself._init_from_description(OPENARM_DESCRIPTION, …). (L512)_openarm_arm_joint_specs(names, position_limits, side) -> list[JointSpec],_openarm_gripper_joint_spec(name, side, position_limits) -> JointSpec,_openarm_joint_specs() -> list[JointSpec](L167, L190, L206)- module const
_OPENARM_LEFT_ARM_JOINTS: tuple[str, ...](L106) — left-arm joint names. - module const
_OPENARM_RIGHT_ARM_JOINTS: tuple[str, ...](L107) — right-arm joint names. - module const
_OPENARM_LEFT_GRIPPER_JOINT: str(L108) — left gripper joint name. - module const
_OPENARM_RIGHT_GRIPPER_JOINT: str(L109) — right gripper joint name. - module const
_OPENARM_JOINT_NAMES: tuple[str, ...](L111) — full 16-DoF joint order. - module const
_OPENARM_LEFT_ARM_POSITION_LIMITS: dict[str, tuple[float, float]](L122) - module const
_OPENARM_RIGHT_ARM_POSITION_LIMITS: dict[str, tuple[float, float]](L131) - module const
_OPENARM_LEFT_GRIPPER_POSITION_LIMITS: tuple[float, float](L143) - module const
_OPENARM_RIGHT_GRIPPER_POSITION_LIMITS: tuple[float, float](L144) - module const
_OPENARM_ARM_EFFORT_LIMITS: dict[str, float](L150) - module const
_OPENARM_GRIPPER_EFFORT_LIMIT: float(L166) - module const
_OPENARM_ARM_VELOCITY_LIMIT: float(L169) - module const
_OPENARM_GRIPPER_VELOCITY_LIMIT: float(L170) - const
OPENARM_DESCRIPTION = RobotDescription(...)(L250) — Shared baseline for sim and real (name="openarm_v2", 16 revolute joints).sdk_kind="open"(the whole real path —openarm_can+openarm_ros2— is Apache-2.0, unlike UR/Franka's closed vendor runtime).hal.sim=None(derivedMujocoArmHAL, so the manifest'shead_zedsensor andcollision_geometryreach the twin) +hal.real_bringup="openral_hal_openarm:real_bringup.launch.py". One flathal.parameters.defaultsblock serves both entrypoints;build_haldrops the keys each constructor doesn't accept. Drift-guarded againstrobots/openarm/robot.yaml.
python/hal/src/openral_hal/openarm_real.py
Real-hardware adapter for the Enactic OpenArm v2. Commands openarm_bringup's ros2_control stack (750 Hz) rather than SocketCAN directly: Skill → Action → this adapter → four command topics → controller_manager → openarm_hardware SystemInterface → openarm_can → SocketCAN → Damiao motors.
- module const
_LEFT_ARM_CONTROLLER: str(L93) — default left-armJointTrajectoryControllername. - module const
_LEFT_GRIPPER_CONTROLLER: str(L94) — default left-gripper controller name. - module const
_RIGHT_ARM_CONTROLLER: str(L95) — default right-arm controller name. - module const
_RIGHT_GRIPPER_CONTROLLER: str(L96) — default right-gripper controller name, matching the udev-pinned CAN dev-id convention. - module const
_JOINT_STATE_TOPIC: str(L97) - module const
_LEFT_CAN_INTERFACE: str(L98) — default left CAN interface name. - module const
_RIGHT_CAN_INTERFACE: str(L99) — default right CAN interface name. - module const
_LEFT_ARM_SLICE(L102) — left-arm slice of the 16-DoF action vector. - module const
_LEFT_GRIPPER_SLICE(L103) — left-gripper slice. - module const
_RIGHT_ARM_SLICE(L104) — right-arm slice. - module const
_RIGHT_GRIPPER_SLICE(L105) — right-gripper slice. class OpenArmRealHAL(RosControlHAL)(L120) — 16-DoF real-HW adapter, same action layout asOpenArmMujocoHAL. Guards three silent-failure modes: fan-out (one action sliced atomically across the bimanual bringup's four controller topics, so a rejection can't leave one arm on a new chunk and the other stale), read-by-name (read_statematches/joint_statesby joint name, raising rather than zero-filling a missing joint), and bus preflight (connect()refuses a missing/wrong-type/down CAN link rather than reporting connected over a dead bus).estop_recovery = RESETTABLE.__init__(description=None, *, left_can_interface='openarm_left', right_can_interface='openarm_right', left_arm_controller=..., left_gripper_controller=..., right_arm_controller=..., right_gripper_controller=..., joint_state_topic='/joint_states', publish_fn=None, state_fn=None, staleness_limit_s=None, require_can_links=True)— Rejects a manifest that is not 16-joint or whose joints lacksim_joint_name(the URDF joint name, i.e. the logical→ros2_control mapping).connect() -> None(L302) — Bus preflight (openral_core.can.preflight_can_links) then delegates toRosControlHAL.connect; refuses a CAN link that is missing, is not a CAN device, or is down.ros2_control_joint_names() -> list[str](L258) — 16 URDF names in action-vector order.controller_names() -> list[str]— The four controllersestopdeactivates, in fan-out order (left arm, left gripper, right arm, right gripper); the fleet conformance test pins that everycommand_bindingstopic belongs to one of them. (L290)estop_recovery = RESETTABLE— the deactivated controllers hold and the CAN bus stays configured, so the inheritedreset_estopre-activates all four through the samecontroller_managerseam and reconnects only once the manager confirms every oneactive.read_state() -> JointState(L335) — Matches/joint_statesby ros2_control joint name and returns manifest order; raises rather than zero-filling when a joint is absent. An absent or wrong-lengtheffortchannel yieldseffort=[], never zeros (a zero-filled channel would fake an effort reading that does not exist).command_bindings() -> dict[str, ControllerKind](L275) — The four/…/joint_trajectorytopics, allJOINT_TRAJECTORY:openarm_bringupconfigures each gripper as a 1-DoFJointTrajectoryController, so grippers and arms take the same message.health() -> HALHealthReport(L555) — Cached per-bus state fromconnect(); performs no I/O.send_action(action)(L393) — ADR-0102 slot groups: atick_group_size > 1action is staged (SlotGroupStager, which refuses a tick at or belowlast_committed_tick) and composed into one 16-DoF action bycompose_slot_group_actionbefore the same all-or-nothing publish; the tick is committed only after all four messages went out. An ungrouped ticked action (starting-pose ramp, approach) is checked and committed against the same watermark (SlotGroupStager.admit/commit_tick). A standalone gripper action raisesROSConfigError— the four bimanual controllers share one joint vector, so there's nowhere to route it. All four messages carry the sametime_from_start_sfromRosControlHAL.time_from_start_s(issue #303).- (property)
last_committed_tick -> int— Tick of the last slot group published (0 = none since disconnect); lets the lifecycle node acknowledge a grouped tick on commit instead of counting slots. (L508) - (property)
last_committed_session -> int—runner_session_idoflast_committed_tick(0 = none / session-less). (L518) - (property)
last_applied_action -> Action | None(L497) — The command last published to the four controllers: the composed 16-DoF action for a slot group, the action itself otherwise; read from theSlotGroupStager, which records it only after the publish, in step withlast_committed_tick. Fed to the grasp trigger by the lifecycle node. estop() -> None(L536) — Discards the half-staged tick (keeping the committed watermark, so a pre-stop tick replayed after the stop is still refused) before delegating, so a slot staged when the stop landed cannot outlive it and corrupt the first post-e-stop tick. The downstream stop itself (all four controllers deactivated and confirmed) is the base implementation's.discard_staged_slots() -> int(L522) —SlotGroupStager.discard(watermark kept); called by the HAL lifecycle node on every/openral/estoplatch and/openral/estop_cleared. Returns the slots dropped.disconnect() -> None(L486) — Also resets the stager (and its committed watermark), so a slot staged before disconnect cannot raise an incomplete-group error on the first tick after reconnect.- Real hardware needs an upstream patch.
openarm_bringupgenerates the realrobot_descriptionvia xacro, and on OpenArm v2.0 theleft_can_interface/right_can_interfacearguments are silently dropped, falling back tocan1/can0(v1.0 is unaffected). Unpatched, the arms don't move and the CAN preflight passes because those buses really are up — it's the description pointing elsewhere.robots/openarm/patches/carries the fix and an idempotentapply.sh. - The bus preflight is not implemented here —
connect()delegates toopenral_core.can.preflight_can_links, passing the two udev-named interfaces and OpenArm-specific remedy text; the mechanism lives in layer 0 for other CAN robots to reach. Narrower thanopenral_detect.probes.can, the richer read behindopenral detect. - const
OPENARM_REAL_DESCRIPTION = make_real_description(OPENARM_DESCRIPTION, sdk_kind="open")(L117) — Shares kinematics, safety envelope and HAL entrypoints with the sim baseline.
python/hal/src/openral_hal/_slot_group.py
Reassembles a slot-dispatched inference tick into one whole-robot command (ADR-0102): _dispatch_slots emits one Action per non-discard slot (four for OpenArm v2 bimanual), each published separately through the safety kernel, so a HAL that commands one vector must recombine them.
- module const
GRIPPER_MODES(L46) —(GRIPPER_POSITION, GRIPPER_BINARY), the modes composed fromAction.gripper+ee_namerather than from a joint row. compose_slot_group(actions, joint_names) -> list[float](L60) — Merges one tick's slots into a full-dof target row, addressing by name (never position):JOINT_*slots viaAction.joint_names, gripper slots viaee_name. RaisesROSConfigErrorfor an unnamed joint slot, an unknown joint/end-effector, a joint claimed twice, or any joint left uncommanded; the placement itself isslot_group_targets, after refusing any mode it cannot place.slot_group_targets(actions, joint_names) -> list[float | None](L113) —compose_slot_group's placement with the same strictness for a group that need not cover every joint: skips jointless slots (BODY_TWIST, Cartesian,FOOT_PLACEMENT,COMPOSITE_MODE; private_JOINTLESS_MODES), returnsNonefor uncommanded joints, and raisesROSConfigErrorfor any unreadable joint-addressing slot (no payload /ee_name/joint_names, unknown name, row too short, a non-position joint-space mode) or a joint written twice — so aNonemeans "not commanded", never "commanded unreadably". Used bysim_attached.gripper_targets_action.compose_slot_group_action(group, joint_names) -> Action—compose_slot_groupwrapped into the one-step full-dofJOINT_POSITIONAction(first slot's stamp/confidence) thatMujocoArmHALandOpenArmRealHALapply. (L214)refuse_stale_tick(tick, last_committed) -> int— The session-less slot-group tick rule (hazard log Entry 036), applied byTickWatermarkonly to actions with norunner_session_id(0): raisesROSRuntimeError("stale slot group") when0 < tick <= last_committed(a replayed group of an already-committed tick), EXCEPTtick == 1whilelast_committed > 1, a restarted runner's fresh numbering, which is adopted by returning0.SlotGroupStageronly checks with it and moves its watermark on commit, so one delayed tick-1 slot of the old runner cannot drop the watermark before a new group is applied. A replay of tick 1 after tick 1 committed is refused. Reached throughTickWatermark.check. (L238)class TickWatermark(L289) — The committed(runner_session_id, tick_index)every slot-group HAL refuses replays against (Entry 036 Amendment 3). Same session:tick <= tickwatermark is refused, tick 1 included. A different non-zero session is a new runner: admitted, adopted only bycommit(never bycheck, so a stray slot cannot move it), and on adoption the old session is retired — every later action carrying it raises ("superseded"). Session-less 0 falls back torefuse_stale_tick.reset()(disconnect) clears the watermark but keeps retired sessions refused. Attributestick,session. Shared bySlotGroupStagerandSimAttachedHAL.check(tick, *, session) -> None(L331) — Raise on a replay; the watermark does not move.commit(tick, *, session) -> None(L354) — Record an applied(session, tick); retires the previous session on a change.reset() -> None(L361) — Clear the watermark, keep the retired set.class SlotGroupStager(L367) — Buffers at most one tick, keyed by(runner_session_id, tick_index), and holds the committed watermark (TickWatermark).stage(action) -> list[Action] | Nonereturns the group when the last slot lands, elseNone; it first refuses a replay (TickWatermark.check); the watermark moves, and a new runner session is adopted, only oncommit(group).admit(action)/commit_tick(action)apply the same watermark to UNGROUPED ticked actions (the runner's starting-pose ramp and approach share its tick counter), so a restarted runner's ramp renumbers the HAL instead of leaving its first policy tick under the old watermark.discard()(E-stop) keeps the watermark, so a pre-stop tick replayed after the stop is refused;reset()(disconnect) clears it. A tick change mid-group raisesROSRuntimeErrorrather than committing survivors and leaving one arm stale; the slot that exposed it opens the new tick instead of being dropped with it. A slot from a different non-zero runner session instead drops the old session's partial tick silently (loggedhal.slot_group.stale_partial_dropped) — a dead run's half tick is nobody's loss. MirrorsSimAttachedHAL._stage_action_group. Used by everyMujocoArmHALtwin andOpenArmRealHAL.admit(action: Action) -> None(L441) — Check an ungrouped ticked action against the watermark before it is applied (TickWatermark.check); the watermark does not move.commit_tick(action: Action) -> None(L459) — Record that an ungrouped action was applied: moves the watermark to its(runner_session_id, tick_index)when ticked, and always records it aslast_applied_action.stage(action) -> list[Action] | None(L493) — see above.pending -> int[@property] (L406) — count of slots in flight.last_committed_tick -> int[@property] — tick of the last groupcommitted (0 = none sincereset). (L411)last_committed_session -> int[@property] — itsrunner_session_id(0 = none / session-less). (L416)commit(group, *, applied: Action) -> None— record that a released group was applied asapplied(the composed command sent); call only after the apply succeeded (the lifecycle node acks this tick). (L430)last_applied_action -> Action | None[@property] — the command the lastcommit/commit_tickapplied (Nonesincereset;discardkeeps it). The one accessor the stager-backed HALs expose as their ownlast_applied_action. (L421)discard() -> int— drop the buffered slots, keep the watermark (estop latch / clear); returns how many slots were dropped. (L469)reset() -> None(L483) — drop the buffered slots, the watermark andlast_applied_action(disconnect).
python/hal/src/openral_hal/anvil_openarm_v2.py
MuJoCo digital twin for the Anvil OpenARM 2.0 — Anvil Robotics' manufactured variant of the standard OpenArm v2. Differs from the Enactic v2 twin only in two joint ranges (J1, J6) and a wrist support bracket, baked into the fetched MJCF, so the HAL stays a thin manifest-driven subclass.
class AnvilOpenArmV2MujocoHAL(MujocoArmHAL)— 16-DoF (7 arm + 1 gripper per side) bimanual HAL drivingmodels/anvil_openarm_bimanual.xmlfrom the pinnedbensonlee5/anvil-openarm-mujococlone. All wiring lives inANVIL_OPENARM_V2_DESCRIPTION.sim. Structurally identical toOpenArmMujocoHAL— the Anvil-ness is entirely in the asset. (L370)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)— Forwards toself._init_from_description(ANVIL_OPENARM_V2_DESCRIPTION, …). (L411)_anvil_arm_joint_specs(names, position_limits, side) -> list[JointSpec],_anvil_gripper_joint_spec(name, side, position_limits) -> JointSpec,_anvil_joint_specs() -> list[JointSpec](L160, L184, L204)- module const
_ANVIL_LEFT_ARM_JOINTS: tuple[str, ...](L89) — left-arm joint names. - module const
_ANVIL_RIGHT_ARM_JOINTS: tuple[str, ...](L90) — right-arm joint names. - module const
_ANVIL_LEFT_GRIPPER_JOINT: str(L91) — left gripper joint name. - module const
_ANVIL_RIGHT_GRIPPER_JOINT: str(L92) — right gripper joint name. - module const
_ANVIL_LEFT_ARM_POSITION_LIMITS: dict[str, tuple[float, float]](L103) — left-arm per-joint position limits, incl. the Anvil-specific J1/J6 ranges. - module const
_ANVIL_RIGHT_ARM_POSITION_LIMITS: dict[str, tuple[float, float]](L112) — right-arm per-joint position limits. - module const
_ANVIL_LEFT_GRIPPER_POSITION_LIMITS: tuple[float, float](L122) - module const
_ANVIL_RIGHT_GRIPPER_POSITION_LIMITS: tuple[float, float](L123) - module const
_ANVIL_ARM_EFFORT_LIMITS: dict[str, float](L128) — per-joint effort limit table. - module const
_ANVIL_GRIPPER_EFFORT_LIMIT: float(L144) - module const
_ANVIL_ARM_VELOCITY_LIMIT: float(L148) - module const
_ANVIL_GRIPPER_VELOCITY_LIMIT: float(L149) - const
ANVIL_OPENARM_V2_DESCRIPTION = RobotDescription(...)(L215) — Sim baseline (name="anvil_openarm_v2", 16 revolute joints, Anvil's J1/J6 ranges, two wrist RGBSensorSpecs).sdk_kind="open",hal.sim=None(derivedMujocoArmHAL),hal.real=None(sim-only; a real driver wrapper is a tracked follow-up). Drift-guarded againstrobots/anvil_openarm_v2/robot.yaml.
python/hal/src/openral_hal/_pinned_clone.py
fetch_pinned_clone(repo_url, sha, repo_dir, *, submodule=None, what=…) -> None— The shared staged-clone + atomic-rename dance for pinned-SHA asset fetchers: shallow clone into a staging dir, checkout the pinned SHA, optional submodule init,os.renameinto place (loser of a concurrent race discarded). RaisesROSConfigErrorwhengitis missing or any step fails. Reuse watch: the canonical pinned-clone fetcher. (L30)
python/hal/src/openral_hal/_anvil_openarm_v2_assets.py
Vendor the Anvil OpenARM 2.0 MJCF from bensonlee5/anvil-openarm-mujoco — no upstream package ships the Anvil variant.
ensure_anvil_openarm_v2_mjcf() -> str— Idempotently clonesbensonlee5/anvil-openarm-mujocoat a pinned SHA, initialises its mesh submodule, and returns the bimanual MJCF path. RaisesROSConfigErrorifgitis missing or the clone/submodule init fails. Mirrors_openarm_v2_assets.ensure_openarm_v2_mjcfplus the submodule step. (L60)- module const
_ANVIL_PINNED_SHA: str(L40) — bump when the generator or the local Anvil spec changes. - module const
_ANVIL_REPO_URL: str(L41) — the pinned clone's URL. - module const
_ANVIL_MJCF_REL: str(L42) — the bimanual MJCF's path within the clone. - module const
_ANVIL_MESH_SUBMODULE: str = "upstream/openarm_mujoco"(L46) — mesh submodule the generated MJCF's meshdir points into; initialised byensure_anvil_openarm_v2_mjcf.
python/hal/src/openral_hal/_openarm_description_assets.py
Fetch Enactic's public openarm_description ROS package (Apache-2.0) — the package://openarm_description/... meshes robots/openarm/openarm.urdf references — for hosts without a ROS workspace that builds it.
ensure_openarm_description() -> Path(L30) — Idempotently clonesenactic/openarm_descriptionat a pinned SHA into$OPENRAL_CACHE_DIR/openarm_description/<sha>/and returns the package root. RaisesROSConfigErrorwhen the clone fails or has nopackage.xml. Registered inopenral_hal.ros_package_overlay.PUBLIC_ROS_PACKAGES(Isaac import + Foxglove asset fetch).
python/hal/src/openral_hal/ros_package_overlay.py
Make fetched public ROS description packages resolvable by ROS tools (resource_retriever in foxglove_bridge), which find package:// roots only through the ament index.
PUBLIC_ROS_PACKAGES: dict[str, Callable[[], Path]](L31) — known public description packages → fetcher returning the package root (openarm_description→ensure_openarm_description). The one registry;openral_sim.backends.isaac_sim._fetch_public_packagereads it.fetch_public_package(pkg) -> Path | None(L38) — the known public package's root (fetched if needed),Nonefor any other.public_package_overlay(urdf_xml) -> Path | None(L57) — an ament prefix at$OPENRAL_CACHE_DIR/ament_overlayregistering every public package the URDF'spackage://URIs name that noAMENT_PREFIX_PATHentry indexes;Nonewhen none is needed.deploy_e2e.launch.pyprepends it tofoxglove_bridge'sAMENT_PREFIX_PATHso the 3D panel can fetch the URDF meshes.
python/hal/src/openral_hal/_openarm_v2_assets.py
Vendor the upstream enactic/openarm_mujoco v2 MJCF until robot_descriptions bumps its own pin to match.
ensure_openarm_v2_mjcf() -> str— Idempotently clonesenactic/openarm_mujocoat a pinned v2 SHA into$OPENRAL_CACHE_DIR/openarm_v2/<sha>/, returns the bimanual MJCF path. RaisesROSConfigErrorwhengitis missing or the clone fails. Mirrors the pattern used bypython/sim/src/openral_sim/backends/so100_robosuite/_assets.py. (L57)- module const
_OPENARM_V2_PINNED_SHA: str(L40) — bump to track upstream v2 updates. - module const
_OPENARM_REPO_URL: str(L41) — the pinned clone's URL. - module const
_OPENARM_V2_MJCF_REL: str(L42) — the bimanual MJCF's path within the clone.
python/hal/src/openral_hal/g1.py
MuJoCo digital twin for the Unitree G1 humanoid. The default stock-Menagerie path provides joint-contract validation + ADR-0087 kinematic glide; explicit walking_enabled=True selects ADR-0089's pinned MuJoCo Playground ONNX policy and matching gravity-on dynamics.
class G1MujocoHAL(MujocoArmHAL)— 29-DoF humanoid HAL; takesdescription=(loaded manifest;None→G1_DESCRIPTION). Default mode drivesmujoco_menagerie/unitree_g1/g1.xml; walking mode swaps in the pinned policy-tuned MJCF + ONNX controller. Floating-base joint remains implicit world state. (L379)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5, body_twist_dt_s=0.05, walking_enabled=False)(L418)connect() -> None(L492) — Connect and re-arm the base pin from the fresh qpos; in walking mode, resets to theknees_bentkeyframe and constructs/initializes theG1WalkingController. RaisesROSConfigError(and disconnects) if the walking MJCF lacks that keyframe.disconnect() -> None(L512) — Release the walking controller, then the MuJoCo model.base_pose -> tuple[float, float, float](property) — Current glide pin or live walking base pose. (L480)base_twist -> tuple[float, ...](property) — Last commanded 6-vec base twist (base_linkframe), zeroed by any non-BODY_TWIST action. (L488)send_action(action)— RoutesBODY_TWISTto the glide or walking controller; every other mode defers to the joint path. Non-planar twist components raiseROSConfigError. (L517)idle_step(wall_dt_s=None) -> bool— HOLD-step with the base pinned; walking state resets so a stale velocity command is never replayed. Opts out of the bare-arm_step_while_activepath since an active BODY_TWIST must not be overridden by HOLD. (L564)reset_to_pose(pose: list[float]) -> None(L576) — Reset the joint pose plus the base pin and (when walking) the gait state._per_step_update(targets)/_pin_base()/_pin()— The upright pin:qpos[0:7]fixed to the glide pose (roll/pitch clamped to 0),qvel[0:6]zeroed. Replacing the pin with a balance controller is the designed S0 upgrade seam. (L604)- module const
_PLANAR_TWIST_EPS(L80) — tolerance for detecting a non-planarBODY_TWISTcomponent (send_action's validation). - module const
_G1_LEFT_LEG_JOINTS: tuple[str, ...](L92) — left-leg joint names. - module const
_G1_RIGHT_LEG_JOINTS: tuple[str, ...](L100) — right-leg joint names. - module const
_G1_WAIST_JOINTS: tuple[str, ...](L108) — waist joint names. - module const
_G1_LEFT_ARM_JOINTS: tuple[str, ...](L113) — left-arm joint names. - module const
_G1_RIGHT_ARM_JOINTS: tuple[str, ...](L122) — right-arm joint names. - module const
_G1_JOINT_NAMES: tuple[str, ...](L131) — full 29-DoF joint order (legs + waist + arms). - module const
_G1_POSITION_LIMITS: dict[str, tuple[float, float]](L150) — per-joint position limits. - module const
_G1_VELOCITY_LIMITS_BY_GROUP: dict[str, float](L186) — per-kinematic-group velocity limits. - module const
_G1_EFFORT_LIMITS_BY_GROUP: dict[str, float](L195) — per-kinematic-group effort limits. _g1_group(joint_name) -> str— Return the kinematic group token (hip/knee/ankle/waist/shoulder/elbow/wrist) forjoint_name. (L209)_g1_parent_child(joint_name) -> tuple[str, str]— Return(parent_link, child_link)for a G1 joint, following the menagerie URDF convention. (L214)_g1_joint_specs() -> list[JointSpec]— Build the 29JointSpecs from the joint-name tuples and the per-joint limit tables. (L264)- const
G1_DESCRIPTION = RobotDescription(...)(L292) — Sim baseline;sdk_kind="open",hal.sim="openral_hal.g1:G1MujocoHAL",hal.real=None(sim-only); in-code mirror —build_halpasses the loadedrobots/g1/robot.yamlasdescription=. Advertisessupported_control_modes=[joint_position, body_twist],mobile_baseinembodiment_tags, and a forwardheadRGB camera so BODY_TWIST nav skills (InternVLA-N1) match. All MuJoCo wiring lives inG1_DESCRIPTION.sim; drift-guarded againstrobots/g1/robot.yaml.
python/hal/src/openral_hal/_g1_walking.py
ADR-0089's private, sim-only walking implementation.
ensure_g1_walking_assets() -> tuple[str, str](L84) — downloads four files from pinned MuJoCo Playground commit43d180a, verifies SHA-256, links the cached Menagerie meshes, flattens the included MJCF, and returns(mjcf_path, policy_path).- module const
_PINNED_SHA: str(L19) — pinned MuJoCo Playground commit. - module const
_RAW_ROOT: str(L20) — raw-GitHub root URL derived from_PINNED_SHA. - module const
_ASSETS: dict[str, str](L21) — the four pinned-asset relative paths mapped to their expected SHA-256 hex digests, verified byensure_g1_walking_assets. class G1WalkingController(L128) — 50 Hz CPU ONNX inference over 500 Hz MuJoCo physics; builds the upstream 103-D observation, validates the 29-D finite output, scales/clips joint targets, and persists gait phase/action history.initialize(data: mujoco.MjData) -> None(L157) — Capturedata.qpos[7:]as the standing default-angle reference, thenreset_state().reset_state() -> None(L161) — Zero the last-action history and gait phase, reset the decimation counter.apply(model: mujoco.MjModel, data: mujoco.MjData, command: tuple[float, float, float]) -> None(L166) — Every_CONTROL_DECIMATION-th physics step, build the 103-D observation (base linear velocity, gyro, projected gravity, command, joint-angle delta, joint velocity, last action, phase sin/cos), run the ONNX policy, and scale/clip/apply the resulting joint targets.
python/hal/src/openral_hal/so100_mujoco.py
MuJoCo digital twin for the SO-100 follower arm (Menagerie MJCF).
class SO100MujocoHAL(MujocoArmHAL)— SO-100 follower MuJoCo HAL, drivingmujoco_menagerie'strs_so_arm100/so_arm100.xmlwith the same 6-DoF action layout asSO100FollowerHAL. Maps lerobot-style joint names to the Menagerie joints and normalises the revolute Jaw range[-0.174, 1.75]to[0, 1]. (L65)__init__(*, mjcf_path=None, settle_steps=None, gravity_enabled=True, staleness_limit_s=0.5)(L106)
python/hal/src/openral_hal/so100_sim.py
SO100DigitalTwin — in-process simulator for the SO-100 follower arm.
- module const
_JOINT_NAMES: list[str](L37) — canonical joint order, mirrors lerobot's SO100Follower motor dict. - module const
_DEFAULT_POSITIONS: dict[str, float](L48) — default joint positions in lerobot's native units (degrees; gripper[0, 100], mid-range). class SO100DigitalTwinConfig(RobotConfig)— Config for the digital twin. (L59) field:initial_positionsclass SO100DigitalTwin(Robot)— In-process digital twin. (L76)__init__(config)(L101)observation_features() -> dict[str, type]— One float per joint pos. (L116)action_features() -> dict[str, type]— One float per target. (L125)is_connected -> bool[@property] (L134)is_calibrated -> bool[@property] — Always True. (L139)connect(calibrate=True) -> None— Activate (no serial port opened). (L146)calibrate() -> None— No-op. (L154)configure() -> None— No-op. (L158)get_observation() -> RobotObservation— Lerobot-native units. (L162)send_action(action) -> RobotAction— Apply position cmd, update state. (L175)disconnect() -> None— Deactivate (idempotent). (L194)
python/hal/src/openral_hal/ros_control.py
RosControlHAL — ros2_control-backed HAL adapter, plus the typed downstream e-stop seam every ros2_control robot stops through (issue #295).
class ControllerKind(StrEnum)— Which ros2_control controller sits behind one command topic, and therefore which message type commands it:JOINT_TRAJECTORY(trajectory_msgs/JointTrajectory— every ros2_control robot in this repo, OpenArm's 1-DoF grippers included) andFORWARD_COMMAND(std_msgs/Float64MultiArray, theforward_command_controllerfamily; no in-repo robot yet). Declared rather than assumed because publishing the wrong type is silent — DDS never delivers it, so the arm does not move and nothing logs an error. Re-exported fromopenral_hal. (L72)class ControllerSwitchReport— Frozen dataclass: acknowledged outcome of onecontroller_managerswitch.okis the service's verdict and the post-switch confirmation (every controller listed in the requested state);statesis whatlist_controllersreported per name;detailnames what did not move. (L108)class TriggerReport— Frozen dataclass:success+messageof onestd_srvs/Trigger-shaped vendor call. (L125)class DownstreamStopReport— Frozen dataclass: what a HAL knows about its last downstream stop —stopped(only when controller deactivation and the vendor stop were acknowledged; a local latch never counts),controllers,controller_states,vendor_stop(service/topic label or""),detail.fields()flattens it for/diagnostics(downstream_stop=acknowledged|unacknowledged, …). Read by the lifecycle node, which logs FATAL on an unacknowledged stop. (L133)fields() -> dict[str, str]— Flattens the report for/diagnosticsand the log line:downstream_stop=acknowledged|unacknowledged,downstream_controllers,downstream_controller_states,downstream_vendor_stop,downstream_stop_detail. (L151)class ControllerStopSeam(Protocol)— What a transport must expose for a ros2_control HAL to stop its controllers:deactivate_controllers(names, *, timeout_s),activate_controllers(names, *, timeout_s)(both →ControllerSwitchReport),call_trigger(service, *, timeout_s) -> TriggerReport,publish_empty(topic). Production:RosControlTransport; unit lane:SimTransport. (L165)deactivate_controllers(names, *, timeout_s) -> ControllerSwitchReport— The stop: switch every named controller toinactiveand confirm. (L178)activate_controllers(names, *, timeout_s) -> ControllerSwitchReport— TheRESETTABLEre-arm: switch toactiveand confirm. (L184)call_trigger(service, *, timeout_s) -> TriggerReport— One declaredstd_srvs/Triggervendor service. (L190)publish_empty(topic) -> None— Onestd_msgs/Emptyon a declared vendor-stop topic. (L194)class ControllerStoppable(Protocol)— The stop-side counterpart ofRosControlDrivable:controller_names(),vendor_stop_services(),vendor_stop_topics(),attach_controller_stop(seam). The lifecycle node refuses to configure aRosControlDrivablereal HAL that is not also this;RosControlHALanswers all four. Pinned per manifest bytests/unit/test_real_hal_estop_fleet_conformance.py. (L200)controller_names() -> list[str]— Everycontroller_managercontrollerestopmust deactivate. (L212)vendor_stop_services() -> list[str]—std_srvs/Triggerservices the vendor stop calls. (L216)vendor_stop_topics() -> list[str]—std_msgs/Emptytopics the vendor stop publishes on. (L220)attach_controller_stop(seam) -> None— Bind theControllerStopSeamat wire-up. (L224)class DownstreamStopReporting(Protocol)—last_stop_report -> DownstreamStopReport | Noneproperty; what the lifecycle heartbeat and FATAL log read. (L230)- (property)
last_stop_report -> DownstreamStopReport | None(L234) class RosControlHAL—ros2_control-backed HAL adapter. ImplementsLifecycleEStopHALwithestop_recovery = RESTART_REQUIREDby default (the conservative policy; OpenArm opts intoRESETTABLE). (L261)__init__(description, controller_name, *, joint_state_topic='/joint_states', command_topic=None, publish_fn=None, state_fn=None, staleness_limit_s=None, stop_timeout_s=5.0)(L324) —staleness_limit_s=Nonereadsdescription.safety.joint_state_staleness_limit_sviaresolve_staleness_limit_s(no constructor default; refused when undeclared). Reads the control rate fromdescription.action_spec.control_freq_hz(the field the runner ticks at and the recorder stamps as fps; every committed ros2_control manifest declares 30 Hz); refuses to construct withROSConfigErrornaming the field when it is missing or not positive, sobuild_hal(mode="real")and the lifecycle node's configure stop there (issue #303).attach_transport(publish_fn, state_fn, stamp_fn=None) -> None— Bind a live transport after construction;build_halruns before any ROS node exists, so a real deployment cannot pass one to__init__.stamp_fnis the transport's per-message arrival clock and is what makes the staleness check a freshness check rather than a time-since-connect check. Rejects a non-callablestamp_fnat wire-up. (L375)attach_controller_stop(seam) -> None— Bind theControllerStopSeam(same rationale asattach_transport); rejects a non-seam at wire-up withROSConfigError. (L414)controller_names() -> list[str]— Everycontroller_managercontrollerestopdeactivates;[controller_name]by default, overridden by the bimanual OpenArm alongsidecommand_bindings. (L437)vendor_stop_services() -> list[str]—std_srvs/Triggerservices andstd_msgs/Emptytopics the vendor stop uses; declared up front so the transport creates the clients/publishers at wire-up and refuses an undeclared name loudly. Empty in the base. (L446)vendor_stop_topics() -> list[str]—std_msgs/Emptytopics the vendor stop publishes on, declared for the same reason. Empty in the base; Sawyer overrides. (L455)- (property)
last_stop_report -> DownstreamStopReport | None(L460) command_bindings() -> dict[str, ControllerKind]— Every controller topic this HAL publishes to, each with the wire format its controller speaks; one entry becomes one typed transport publisher. Insertion order is thesend_actionfan-out order. This is the override point for a new robot — overridden by the bimanual OpenArm (4 topics, allJOINT_TRAJECTORY). (L464)command_topics() -> list[str]— Derived fromcommand_bindings()so the topic list and the declared formats cannot drift; overridecommand_bindings, not this. (L478)ros2_control_joint_names() -> list[str]— Joint names in ros2_control's namespace, action order; what/joint_statesis keyed by. Overridden where URDF names differ from manifest names (OpenArm). (L486)- (property)
joint_state_topic -> str— The aggregatedsensor_msgs/JointStatetopic this HAL reads. (L496) - (property)
controller_name -> str— Name of the primaryros2_controlcontroller this HAL commands (the onecommand_topicdefaults from). Hoisted here from the Franka / Sawyer wrappers so every subclass answers it. (L501) connect() -> None(L507)disconnect() -> None— inherited fromHALBase(flag-and-log default; no extra teardown needed).read_state() -> JointState— Age is measured fromstamp_fn()when a transport supplied one, else fromconnect(). (L527)send_action(action) -> None— Publish JointTrajectory; carriesjoint_namesso no transport keeps a second copy of the mapping, andtime_from_start_sfromtime_from_start_s(action). (L573)time_from_start_s(action) -> float— Trajectory deadlinehorizon / action_spec.control_freq_hz: a chunk is reached just as its replacement is due; the transport spreads the chunk's rows evenly up to it. Issue #303: at 30 Hz that default re-planned a steep segment every 33 ms and amplified each step between targets. (L607)estop() -> None— Drop the connection flag first (nothing more leaves this HAL), then_stop_downstream(), then raiseROSEStopRequestedwhose message says whether the downstream stop was acknowledged. The outcome lives inlast_stop_report, not the exception type, so the HAL Protocol's "alwaysROSEStopRequested" holds. With no seam attached the report isstopped=False/ "no controller stop seam attached" — never a claimed stop. (L630)_stop_downstream() -> DownstreamStopReport[private] — Deactivatescontroller_names()through the seam (STRICT switch + listing confirmation), then runs_vendor_stop; both are attempted even if the first fails, so the report names everything that did and did not acknowledge. (L663)_vendor_stop(seam) -> str[hook] — Vendor-specific stop beyond controller deactivation; returns its label for the report or raises aROSError. Base: none (for a plain ros2_control robot deactivation is the stop; forfranka_hardwareit is what callslibfranka'sstopRobot()). Overridden by UR (dashboardstopTrigger) and Sawyer (super-stop Empty). (L721)_vendor_reset(seam) -> None[hook] — Vendor re-arm before controllers are re-activated; no-op by default. (L733)reset_estop() -> None— Only forRESETTABLEadapters:_vendor_reset, thenactivate_controllersthrough the seam, and only once the manager confirms every controlleractiveis the HAL marked connected again. RaisesROSRuntimeErrorfor aRESTART_REQUIREDpolicy ("in-process reset is forbidden"), a missing seam, or an unacknowledged re-activation — so the lifecycle latch can never clear ahead of the vendor reset. (L737)- private:
_require_connected,_validate_action
python/hal/src/openral_hal/ros_control_transport.py
Production rclpy transport shared by every real RosControlHAL robot.
class RosControlDrivable(Protocol)— runtime-checkable, structural: what a HAL must expose to be driven byRosControlTransport(command_bindings,ros2_control_joint_names,joint_state_topic,attach_transport). The lifecycle node's wiring gates on this, not onisinstance(hal, RosControlHAL), so a ros2_control robot that reimplements the fan-out onHALBaserather than inheriting can opt in by shape instead of being silently skipped. Non-method member (joint_state_topicis a property) soisinstanceworks butissubclassraises — gate withisinstance. (L80)command_bindings() -> dict[str, ControllerKind](L93) — seeRosControlHAL.command_bindings.ros2_control_joint_names() -> list[str](L97) — seeRosControlHAL.ros2_control_joint_names.- (property)
joint_state_topic -> str(L102) attach_transport(publish_fn, state_fn, stamp_fn=None) -> None(L106) — seeRosControlHAL.attach_transport.class RosControlTransport— Bridges oneRosControlHALto live ros2_control topics and implementsControllerStopSeamovercontroller_manager. Built only from what the HAL reports about itself (command_bindings(),ros2_control_joint_names(),joint_state_topic), so a one-controller UR and the four-controller bimanual OpenArm are one code path and a new robot needs no transport code. Wired automatically by the lifecycle node underhal_mode:=real. Reuse watch: the canonical real-HW ros2_control bridge — do not hand-roll publishers in a per-robot HAL. (L128)__init__(node, *, command_topics, joint_names, joint_state_topic='/joint_states', command_kinds=None)— Refuses an empty topic or joint list, and acommand_kindsentry naming a topic not incommand_topics(a typo there would silently leave the real topic on theJOINT_TRAJECTORYdefault). Topics absent fromcommand_kindsdefault toJOINT_TRAJECTORY. (L157)publish(topic, msg) -> None— One command dict → the message type this topic's declaredControllerKindcalls for. Publishes the chunk's final step only (a point is an absolute target the controller interpolates toward). Raises on a topic the HAL never declared, and on a payload it cannot express (nojoint_targetskey — e.g. a{"position": …}gripper command): dropping that quietly would be a silent no-op on the actuation path. An emptyjoint_targetslist is the distinct, benign "nothing to send this tick". (L293)state() -> dict[str, object]— Newest joint state projected onto the HAL's joint order; merged by name, never index, since split controllers publish independently. An unheard joint's position/velocity read 0.0;effortis[]unless every joint carried an effort value. (L405)last_arrival() -> float—time.monotonic()of the newest message; 0.0 before the first. FeedsRosControlHAL.attach_transport(stamp_fn=...). (L430)deactivate_controllers(names, *, timeout_s) -> ControllerSwitchReport— The stop.controller_manager/switch_controllerwithdeactivate_controllers=names,strictness=STRICT, thenlist_controllersto confirm every name readsinactive;okis both. Discovery, the switch and its confirmation share onetimeout_sdeadline, so a stalled manager cannot stretch the stop past the budget. A deactivatedJointTrajectoryControllerholds its last position command (zeroes velocity / effort), drops late trajectories (its subscriber goes inactive) and writes nothing until re-activated — verified againstros2_controllersjazzy, which is also why an empty trajectory is not used as a stop: ROS 2 rejects it ("Empty trajectory received"). (L436)activate_controllers(names, *, timeout_s) -> ControllerSwitchReport— Mirror for aRESETTABLEre-arm; confirmsactive. (L449)call_trigger(service, *, timeout_s) -> TriggerReport— One declaredstd_srvs/Trigger(UR/dashboard_client/stop);ROSConfigErrorfor an undeclared service,success=Falsewhen absent or unanswered; discovery and the call share onetimeout_sdeadline. (L455)publish_empty(topic) -> None— Onestd_msgs/Emptyon a declared vendor-stop topic (Sawyer/robot/set_super_stop);ROSConfigErrorfor an undeclared one. (L487)controller_states(*, timeout_s) -> dict[str, str]—{name: state}fromlist_controllers;{}if the manager did not answer; discovery and the call share onetimeout_sdeadline. The lifecycle node logs a warning at wire-up when the HAL's controllers are not yet listed active. (L504)close() -> None— Destroy the helper node the service clients live on (lifecycle cleanup). (L517)__init__also takescontroller_names,trigger_services,empty_topics(fromhal.controller_names()/vendor_stop_services()/vendor_stop_topics()) andcontroller_manager='/controller_manager'. Service calls run on a private helper node + executor because the e-stop callback runs on the lifecycle node's single-threaded executor and a future issued from inside a callback can only complete if something else spins the client.seen_joints() -> set[str](L586)missing_joints() -> list[str](L590)- private:
_time_from_start_s(msg, topic)— the chunk deadline the HAL supplied; no fallback — a command without a positivetime_from_start_sraisesROSConfigError(a fixed 100 ms constant in its place is the defect of issue #303).publishturns every chunk row into aJointTrajectoryPoint, evenly spaced up to that deadline, so the controller interpolates through the chunk's shape (a single-row tick is one point at one control period)._on_joint_state - module const
_COMMAND_DEPTH = 1(L120) — command QoS depth (RELIABLE, VOLATILE, shallow — CLAUDE.md §2). - module const
_STATE_DEPTH = 10(L125) — state QoS depth. - module const
_STRICT = 2(L76) —controller_manager_msgs/SwitchController.STRICT: a switch that cannot fully apply is refused rather than partially applied. _left(deadline) -> float— Seconds left before atime.monotonic()deadline, floored at zero; every stop-seam phase (wait_for_service, the call, the confirmation) is budgeted through it. (L636)_message_type(kind) -> type— The one place aControllerKindbecomes a ROS message class. Adding an enum member without extending this raises at wire-up rather than publishing a plausible-but-wrong type onto the actuation path;tests/unit/test_ros_control_transport.py::test_every_controller_kind_maps_to_a_message_typepins that. (L641)
python/hal/src/openral_hal/interbotix_transport.py
Production rclpy torque-stop seam for the Interbotix XS arms (ALOHA) — the stop only, not the command path (#250).
class InterbotixXSTransport— Implementsopenral_hal.aloha.InterbotixStopSeamover realinterbotix_xs_msgs/srv/TorqueEnableclients, one perarm_namespacesentry, created at wire-up on a private helper node (same callback-deadlock rationale asRosControlTransport). RaisesROSConfigErrorat construction wheninterbotix_xs_msgsis not installed, so the ALOHA e-stop fails at configure, not on the first stop. Attached by the lifecycle node underhal_mode:=realto anyInterbotixStoppableHAL. (L38)torque_enable(robot_name, *, group, enable, timeout_s) -> TriggerReport—/<robot_name>/torque_enablewithcmd_type='group';TorqueEnablehas an empty response, so the acknowledgement is the call completing — absent or unanswered →success=False.ROSConfigErrorfor an undeclared arm. (L88)close() -> None— Destroy the helper node. (L131)
python/hal/src/openral_hal/sim_transport.py
SimTransport — in-memory simulated ros2_control transport and ControllerStopSeam; SimTorqueSeam — in-memory InterbotixStopSeam. The in-repo controller simulators the unit lane proves the e-stop path on.
class SimTransport— In-memory transport simulating JointTrajectory controllers behind acontroller_managertable. Mirrors the two verified facts about a deactivatedJointTrajectoryController: a command on aninactivecontroller's topic is dropped (recorded indropped_calls, not applied) and nothing moves until re-activated. (L59)__init__(n_joints, *, controllers=None, trigger_responses=None, switch_faults=None)—controllersgiven → STRICT table (an unknown name refuses a switch like an unloaded controller); omitted → names registeractiveon first use.trigger_responsesmaps a service to the(success, message)itscall_triggerreturns (simulate a refusing UR dashboard);switch_faultsmaps"deactivate"/"activate"to the exception that switch raises instead of answering (the rclpy-shaped fault the HAL must contain in its report). (L103)publish(topic, msg) -> None— Record msg, applyjoint_targets— unless the topic's controller is inactive. (L128)state() -> dict[str, object]— Current simulated joint state. (L152)deactivate_controllers(names, *, timeout_s) -> ControllerSwitchReport— Flip the table toinactive; refuse unknown names under STRICT; raise theswitch_faults["deactivate"]exception when configured. (L167)activate_controllers(names, *, timeout_s) -> ControllerSwitchReport— Mirror for the re-arm;switch_faults["activate"]likewise. (L173)call_trigger(service, *, timeout_s) -> TriggerReport— Recorded; answered fromtrigger_responses(default: success). (L179)publish_empty(topic) -> None— Recorded inempty_publishes. (L185)controller_state(name) -> str—active/inactive/unloaded. (L189)call_count -> int[@property] — Number of commands delivered to an active controller. (L196)last_call -> tuple | None[@property] — The most recent delivered(topic, msg). (L201)calls -> list[tuple][@property] — Every delivered(topic, msg), in order. (L206)dropped_calls -> list[tuple][@property] — Every command that reached aninactivecontroller and was dropped. (L211)switch_calls -> list[tuple][@property] — Every("deactivate" | "activate", names)switch, in order. (L216)trigger_calls -> list[str][@property] — Everystd_srvs/Triggerservice called, in order. (L221)empty_publishes -> list[str][@property] — Everystd_msgs/Emptytopic published on, in order. (L226)class SimTorqueSeam— In-memoryInterbotixStopSeam: a simulatedxs_sdktorque table.torque_enable(robot_name, *, group, enable, timeout_s) -> TriggerReportflips the arm's torque, refuses an arm not inarms(absent service) or listed infailing, and raises the exceptionfaultsmaps an arm to (the rclpy-shaped fault the HAL must contain while still stopping the other arms);torque(name)/callsfor assertions. (L266)torque_enable(robot_name, *, group, enable, timeout_s) -> TriggerReport— Record the call and flip the arm's torque unless it is absent, listed infailing, or mapped infaults(then raise that exception). (L306)torque(robot_name) -> bool— Whether the arm is currently torqued on. (L322)calls -> list[tuple[str, str, bool]][@property] — Every(robot_name, group, enable)call, in order. (L327)
python/hal/src/openral_hal/lifecycle.py
Generic ROS 2 managed lifecycle node wrapper for every HAL adapter — UR5e / UR10e / Franka / SO-100 / OpenArm / H1 / future HALs all share the same publish / subscribe / heartbeat / OTel-span wiring.
class HALLifecycleNodeBase(LifecycleNode)— Public base class. Owns the standard/joint_statespublishers, the/openral/safe_action+/openral/estopsubscribers, the 1 HzDiagnosticsHeartbeat, the per-tickhal.read_state/hal.send_actionOTel spans, the estop latch, thepublish_rate_hzparam (0, the default, = the manifest'scontrol_rate_hzso proprio is published once per runner tick; a manifest with noaction_spec.control_freq_hzwarns and uses 30 Hz — issue #303), and the full configure → activate → deactivate → cleanup → shutdown transition wiring. The/openral/action_appliedack is monotonic per runner session, with the same renumbering the HAL adopts: a new non-zerorunner_session_idrenumbers it, and for a session-less runner (0) tick 1 after a higher acked tick resets it (_publish_action_applied_if_complete), so a restarted runner is not left waiting on tick 1. (L538)_create_hal(self) -> HAL— Subclass hook (required): construct and return a HAL instance, reading ROS-parameter-driven constructor args viaself.get_parameter(...). (L638)_heartbeat_extra_fields(self) -> dict[str, str]— Subclass hook (optional): extra key/values for the/diagnosticspayload. Default{}. (L650)on_configure_post_hal(self) -> TransitionCallbackReturn— Subclass hook (optional): robot-specific setup after the HAL connects. DefaultSUCCESS. (L707)on_activate_post_subs(self) -> TransitionCallbackReturn— Subclass hook (optional): robot-specific timers/publishers after the base wires its subs. DefaultSUCCESS. (L716)on_deactivate_pre_teardown(self) -> None— Subclass hook (optional): stop robot-specific timers before base teardown. Default no-op. (L724)on_cleanup_pre_disconnect(self) -> None— Subclass hook (optional): tear down robot-specific resources beforeHAL.disconnect(). Default no-op. (L731)shutdown_hal(self) -> None— Disconnects the HAL on a signal-driven process teardown, sincerclpy's SIGINT handler raises out ofspinwithout requesting the lifecycleshutdowntransition, soon_shutdown/on_cleanup(andHAL.disconnect) would otherwise never run. Called from bothmain()factories'finally, beforedestroy_node; exactly-once and idempotent. Deliberately HAL-only — no publisher/timer teardown, since the rclpy context is already down. SIGKILL stays uncatchable. (L1019)_publish_joint_state(self) -> None— Timer callback: readsself._hal.read_state()under ahal.read_statespan and publishes/joint_states; when the HAL exposesread_policy_state, also publishes/openral/policy_stateonce per newProprioFrame(never a latched republish). Subclasses may override + callsuper()to extend (OpenArm does, for viewer-sync). (L1153)_on_safe_action(self, msg) -> None—/openral/safe_actioncallback. Decodes theopenral_msgs/ActionChunkinto anopenral_core.Actionand forwards via_send_action_traced(action, source="safe_action"). (L1262)_send_action_traced(self, action, *, source) -> bool—Falsewhen no HAL is bound orsend_actionraised (logged on the span), elseTrue; Forwardsactiontoself._hal.send_actioninside ahal.send_actionspan;sourcedisambiguates the origin on the dashboard's Commands card. (L1445)_on_estop(self, msg) -> None—/openral/estopcallback. Ordered latch → stop → report: sets_estopped(so_on_safe_actiondrops commands), calls_invoke_hal_estop, then in afinallydrops any half-staged slot group (_discard_staged_slots) and reports via_emit_estop_telemetry, so an e-stop is counted even if the vendor stop path raises. Nothing is added ahead of the physical stop._discard_staged_slots(self, boundary) -> None— Calls the HAL'sdiscard_staged_slots()when it has one, for every HAL (latch-only ones included — the MuJoCo twin never receivesestop()), on the/openral/estoplatch and on every/openral/estop_cleared(before the latch check); logsopenral_hal.slot_group_discardedwhen slots were dropped, and onceopenral_hal.slot_group_discard_skippedfor a HAL without the method (it stages no groups). Without it a tick a kernel stop cut short survivedopenral estop resetinto the next goal (Thor twin pass, 2026-10-03)._invoke_hal_estop(self) -> None— Callsself._hal.estop()for HALs implementingLifecycleEStopHAL; on the expectedROSEStopRequestedit reads_downstream_stop_fields()and logs ERROR "hardware estop completed" only when the HAL'sDownstreamStopReportsaysacknowledged, else FATAL "hardware estop NOT acknowledged downstream — only the local latch holds"; any other exception is logged at fatal — the latch must survive a vendor stop-path failure._downstream_stop_fields(self) -> dict[str, str]— Flattens the HAL'slast_stop_report(DownstreamStopReporting) for the log line and the/diagnosticsheartbeat:downstream_stop=acknowledged|unacknowledged|unprovenplus controllers / states / vendor stop / detail._heartbeat_statusmerges these into theestop latchedpayload so an operator sees whether the physical stop was proven._emit_estop_telemetry(self) -> None— Emits theopenral.event.estop_requestedspan event and incrementsopenral.hal.estop.count. The only producer of either signal — the dashboard ingests OTLP, not/openral/estoptopics, so this is the sole chokepoint every robot HAL shares on the actuation side. Never raises; telemetry must not disturb the stop path.make_lifecycle_main(node_name, hal_factory) -> Callable[[], None]— Builds amain()entry point for a zero-parameter HAL adapter, via a_FactoryHALLifecycleNodewhose_create_hal()returnshal_factory(). Superseded for standard arms bymake_lifecycle_main_from_manifest; retained for bespoke nodes. (L405)_launched_node_name(default, argv) -> str— The node name a__node:=/__name:=remap inargvsets, elsedefault.make_lifecycle_main_from_manifest'smain()derives the OTelservice.namefrom it, so every robot sharing the oneopenral_hal_nodeexecutable still reports asopenral.hal.<robot_id>. (L384)class ManifestHALLifecycleNode(HALLifecycleNodeBase)— Public generic manifest-driven lifecycle node. Readsrobot_yaml+hal_mode+ sensor knobs as ROS params and builds its HAL viaopenral_hal.build_hal, so a robot's construction kwargs come fromhal.parameters.defaults— no bespoke_create_halsubclass needed.hal_transport_json(a JSON objectdeploy sim|runfills from DeployScenehal.defaults+--hal) carries every constructor kwarg, so one the node does not declare by name (galaxeahost, openarmleft_can_interface) is forwarded, not dropped; the namedport/robot_ip/fci_ip/id/calibration_dirparams win per key; a non-object raisesROSConfigError. AttachesSimSensorBridgeinon_activate_post_subs. Underhal_mode:=realit attaches aRosControlTransportto anyRosControlHALand drops this node's global/joint_statespublisher so the vendor'sjoint_state_broadcasterstays the sole writer; since issue #295 it also attaches that transport as the HAL'sControllerStopSeam(attach_controller_stop, built fromcontroller_names()/vendor_stop_services()/vendor_stop_topics()), refuses to configure (ROSConfigError) a drivable HAL that is notControllerStoppable, warns whencontroller_managerdoes not yet list its controllers active, and attaches anInterbotixXSTransportto anInterbotixStoppableHAL (ALOHA) via_attach_interbotix_transport, closing both seams' helper nodes inon_cleanup; it opens/openral/<robot>/reset_to_poseiff the HAL exposesreset_to_pose, and/openral/<robot>/reset_episode(std_srvs/Trigger, replyseed=<n>; proprio snapshot refreshed and republished; the vision attachment bridge is rebuilt empty, so a payload held in the previous episode is not checked against the new pose) iff it exposesreset_episode(sim-attached HALs only), and subscribes/openral/reward/active_task(std_msgs/String, RELIABLE / VOLATILE / KEEP_LAST 1) into the HAL'sset_active_taskiff it exposes one (_subscribe_active_task). When a scene composition is declared, the named composer's MJCF is threaded in as the HAL'smjcf_path, read from thescene_composition_jsonROS param — which takes precedence over the manifest'sscene_defaults.compositionfallback, so the scene owns its arena and the manifest describes the robot. When the manifest declares a planar base (base_joints), also attaches aMobileBaseBridge. A back-compat alias_ManifestHALLifecycleNodeis retained. (L1727)spin_until_shutdown(node) -> None(L454) —rclpy.spin(node)until a signal-driven shutdown, which ends it quietly:KeyboardInterrupt/ExternalShutdownException, and aRuntimeError(RCLErrorfrom a timer callback that published on the context the SIGINT handler had just invalidated — the HAL exited 1 on Ctrl-C once a depth camera published — or a take racing the shutdown). ARuntimeErrorwith the context still up propagates. Both HALmain()s spin through it.make_lifecycle_main_from_manifest(node_name) -> Callable[[], None]— Builds amain()that spins upManifestHALLifecycleNode, readingrobot_yaml+hal_mode("sim"|"real") ROS params and constructing the HAL viabuild_hal(description, mode=hal_mode)— one node class serves both modes for every robot.deploy sim/deploy runinject the respective mode; a robot lacking it raisesROSCapabilityMismatch. The one caller ispackages/openral_hal_node(node_name="openral_hal_node"); the launch renames the nodeopenral_hal_<robot_id>. (L477)joint_state_republish_stamp_ns(node_now_ns, sample_stamp_ns, *, wall_now_ns=None) -> int(L96) — Stamp for the HAL node's~/joint_statesrepublish: the node clock's now minus the sample's wall-clock age (JointState.stamp_ns), so a stamp-pairing consumer (the robot self-filter'smax_joint_state_skew_s) sees a stale sample as stale. A negative or >5 s age (another clock domain) falls back to the node's now.urdf_named_joint_state(msg, urdf_names) -> JointState(L131) — A copy of a manifest-namedJointStateunder the URDF's joint names (resolver.urdf_joint_names), joints the URDF lacks ("") dropped. Published by the HAL node on~/urdf_joint_stateswhen itsurdf_joint_namesparameter is set (sim twins only; a real ros2_control robot's broadcaster already uses the URDF names), forrobot_state_publisher.sim_attachment_heartbeat(*, hal_mode: str, vision_attachment_enabled: bool) -> bool(L159) — Whether the HAL node buildsSimSensorBridgewith its "nothing attached, fresh" attachment heartbeat: only forhal_mode == "sim"with the vision attachment leg off. On a real arm that claim has no evidence behind it, so real mode publishes no attachment state unless the vision leg is on, and the kernel's attached-payload check fails closed rather than trusting it.twin_jaw_evidence_timeout_s(*, hal_mode: str, hal_idle_steps: bool, idle_hold_s: object, configured_s: float, rate_hz: float) -> float | None(L187) — The vision leg's jaw-evidence timeout on the idle-stepping sim twin:max(configured_s, idle_hold_s + 4 / rate_hz), only forhal_mode == "sim"with a callable HALidle_stepand a floatidle_hold_s;Noneotherwise (the configured timeout stands, and the evidence run resets on gaps). The node setsVisionAttachmentConfig.evidence_run_spans_gapsexactly when it is notNone. The real node also carries aSimSensorBridge, so the bridge's presence must never be the gate (that stretched the real 0.5 s timeout past the kernel's 500 ms attached deadline).decode_action_chunk(msg) -> Action | None— Inverse ofros_publishing_hal._flatten_action_payload: decodes theActionChunkwire shape back into a typedopenral_core.Actionwith the per-mode payload populated. ReturnsNonefor degenerate chunks or modes the F1/F5 publisher doesn't encode (CARTESIAN_POSE,FOOT_PLACEMENT,DEX_HAND_JOINT). Preservestick_index/tick_group_size/runner_session_id(absent session decodes to0, session-less),joint_names(ADR-0102; absent decodes toNone, a whole-vector action) andcartesian_delta_scale(predictive-safety metadata only — the raw delta passes through unchanged). (L240)
python/hal/src/openral_hal/sim_bringup.py
Resolve a SimScene or BenchmarkScene YAML path to a live SimRollout. Used by build_hal, which every manifest-driven node (incl. panda_mobile) routes through.
build_sim_env_from_yaml(sim_env_yaml: str, *, robot_id_fallback: str | None = None) -> tuple[SimRollout, int | None]— Loads aSimScene/BenchmarkSceneYAML, resolves its scene id inopenral_sim.SCENES, and instantiates the env; relative paths resolve against the source file's parents (ROS param values are cwd-naïve). Returns(env, seed)forSimAttachedHAL(env_reset_seed=seed). Binds the robot viaSCENES.resolve_robot(the YAML'srobot_id, else a fixed scene's default, elserobot_id_fallbackfor free-axis scenes). RaisesROSConfigErrorif the YAML is missing, the scene id is unregistered, the robot is not one the scene can build, or validation fails. Scene-id agnostic: continuous-modeignore_doneis the backend'senable_continuous. (L123)hal_transition_timeout_s(deploy_config: str | None) -> str— Per-transition budget for the HAL'stools/lifecycle_autostart.py, derived from the scene:max(300.0, scene.backend_options.boot_timeout_s + 60.0). Falls back to the 300 s floor for no config, an unreadable path, or a malformed value. A sidecar backend boots the simulator insideon_configure, so this bounds the same callbuild_sim_env_from_yamlmakes. (L222)- const
HAL_TRANSITION_TIMEOUT_FLOOR_S = 300.0/HAL_TRANSITION_TIMEOUT_MARGIN_S = 60.0— The floor covers a robocasa-kitchen first boot; the margin covers the import + first reset following a sidecar handshake. Also the ceiling on how long a wedged HAL stays unreported. (L216) - const
HAL_TRANSITION_TIMEOUT_MARGIN_S = 60.0(L219) — see above. _synthesise_deploy_task_id(scene_id) -> str(L53) — The inert"<scene_id>/_hal_deploy_noop"task id for a taskless DeployScene; each backend interprets it (LIBERO → task 0), so no scene-id list lives here.
python/hal/src/openral_hal/sim_attached.py
SimAttachedHAL — generic HAL Protocol adapter that wraps any in-process SimRollout. Shared by panda_mobile, manifest-driven arms, and tests; not import-safe without openral_sim + mujoco.
- module const
_TASK_SUCCESS_LOGGER(L72) — dedicatedopenral.sim.task_successstructlog logger (dotted underopenral.so the OTel bridge attaches). - module const
_EVENT_TASK_SUCCESS(L77) — thesim.task_successevent name_emit_task_successlogs. - module const
_EVENT_TASK_SUCCESS_FINAL(L78) — thesim.task_success_finalevent name, emitted fromdisconnect. - module const
_EVENT_TASK_SUCCESS_PROBE_FAILED(L79) — thesim.task_success_probe_failedevent name, emitted once when the backend's success predicate raises. - module const
_ROBOSUITE_GROUP_PREFIX(L190) — robosuite body-name prefix (robot0_/robot1_) stripped bynormalized_joint_index's prefix-fallback. - module const
_PLANAR_TWIST_EPS(L235) — tolerance for detecting a non-planarBODY_TWISTcomponent. - module const
_PENDING_GROUP_STALE_NS(L242) — how longidle_stepwaits before loudly discarding a pending mid-flight atomic action group. ActionPacker— Type aliasCallable[..., np.ndarray]. Per-composition translator between an OpenRALActionand the env's flat action vector; default factory ispack_action_for_env. The optional trailingprevarg lets a packer carry an untouched slot (e.g. the gripper while the arm steps) across a split policy step. Pass a custom instance toSimAttachedHAL.__init__for whole-body/dexterous-hand layouts. (L252)gripper_targets_action(group: list[Action], description: RobotDescription) -> Action | None(L129) — What a committed slot group set for everyrole: gripperjoint, as a one-row compactJOINT_POSITIONaction (joint_names= every gripper in manifest order) thatVisionAttachmentBridge.observe_commandreads positionally. Placement is_slot_group.slot_group_targets(the composer's own strictness, never a laxer re-implementation). All or nothing:Nonewhen the manifest has no gripper, the group left any gripper uncommanded, or a slot is unreadable (gripper slot with no/unknownee_nameor empty payload, joint row too short for a joint it names) or overlapping — a partial record would leave the uncovered jaw's stale command in the grasp trigger (a phantom attach), whileNoneclears every jaw.SimAttachedHAL._commit_grouprecords it aslast_applied_actionfor a groupcompose_slot_group_actionrefuses (a base twist on a mobile manipulator), so the jaw command still reaches the grasp trigger. A record, never a command to apply.normalized_joint_index(model_joint_names: list[str]) -> dict[str, int]— Maps MJCF joint names (exact + robosuite-prefix-stripped) to model index; exact names always win,robot0_joint1→joint1is added only when it doesn't shadow or collide. Used bySimAttachedHAL.read_stateso one manifest serves both native MjSpec and robosuite scenes. (L193)pack_action_for_env(action: Action, description: RobotDescription, env_action_dim: int, prev: np.ndarray | None = None) -> np.ndarray— DefaultActionPacker: translatesJOINT_POSITION,BODY_TWIST(slots 0-2),CARTESIAN_DELTA(arm slots), andGRIPPER_POSITION(last slot) into the env's flat action vector. RaisesROSConfigErrorfor unsupported modes or mismatched row widths.prevcarries an untouched slot (e.g. gripper) across a policy step that splits into two Actions on a non-composite env — without it each Action zeroed the other's slots. (L270)class SimAttachedHAL— HAL Protocol adapter wrapping an in-processSimRollout. Reads live joint state vianormalized_joint_index+mj_name2id; sends actions via a caller-suppliedActionPacker, else the env's ownpack_action(action, prev)hook when it defines one (a backend that knows its slot layout — the Isaac manifest scene: name-addressed, absolute; also used for its BODY_TWIST path and its slot groups — which may then carry a BODY_TWIST slot, the twist kept across the tick's slots and latched for/odom— withNonerather than zeros as the firstprev), elsepack_action_for_env, intoenv.step().connect()drops the last packed vector, so pre-reset targets never ride along. Exposesread_images(),mujoco_handles(),sim_time_ns(),base_pose,base_twist,base_pose_6dof()for the lifecycle node's camera, viewer, clock, and odom wiring. (L431)__init__(env: SimRollout, description: RobotDescription, *, action_packer: ActionPacker | None = None, env_reset_seed: int | None = None, env_action_dim: int | None = None, body_twist_dt_s: float = 0.05) -> None(L460)connect() -> None— Resets the env atenv_reset_seed; probesenv_action_dimvia_probe_env_action_dim, raisingROSConfigErrornaming the backend if not introspectable and no override given; then_check_time_base. Idempotent. (L630)_check_time_base() -> None(L647) — For a backend that declaressim_dt_per_tick_s(the Isaac sidecar, RoboCasa, LIBERO), refuses viaopenral_core.check_time_base(ROSConfigError) an env whose step is not one control period of the robot (1 / description.control_rate_hz, 1 % tolerance_TIME_BASE_TOL) or a manifest with no control rate to hold it to; records the tick and logssim.time_base(backend,sim_dt_per_tick_s,control_freq_hz,robot) once per connect so a trace carries the time base it was captured on (issue #355). A backend without the field is not checked (RoboCasa/LIBERO step at their own control rate).set_active_task(prompt) -> None— Deploy-sim per-goal placement grading (logging only): forwards the dispatched goal's prompt to the env'sset_task(Isaacplacement_success), then logssim.placement_gradeper grade change (read from stepinfo["placement_grade"]),sim.placement_grade_final(closing + best grade) when the goal ends ("", the next prompt, ordisconnect), andsim.placement_grade_unavailableonce for a prompt the scene does not grade (WARNING + latch off ifset_taskraises). No-op for an env withoutset_task. Fed byManifestHALLifecycleNodefrom/openral/reward/active_task. (L1449)reset_episode() -> int(L681) — Starts a fresh episode on the live env without rebuilding it (sim only): the sameenv.reset+ per-episode bookkeeping asconnect, with seed = base reset seed + episode index (1, 2, …), so per-reset randomisation (Isaacpose_noise) draws a new, reproducible layout each episode. Returns the seed. Served byManifestHALLifecycleNodeas/openral/<robot>/reset_episode.disconnect() -> None— Emits the terminalsim.task_success_finalline, then releases the env handle (idempotent). (L766)read_state() -> JointState— Walksdescription.joints, resolves each vianormalized_joint_index, reads liveqpos/qvelfrom MJCF andeffortviajoint_actuator_effort. Without MuJoCo handles (Isaac sidecar) it readsobs["joint_positions"|"joint_velocities"|"joint_efforts"]. Effort is all or nothing: a joint absent from the MJCF, nojoint_effortskey, or a short vector yieldseffort=[], never zero rows. (L789)send_action(action: Action) -> None(L949) — Packs the action via composite-split orActionPacker, callsenv.step.BODY_TWISTEuler-integrates qpos directly on a MuJoCo backend, else routes through_apply_body_twist_via_env_step. Stampslast_action_nsso the idle stepper yields._stage_action_group(action, group_step) -> None— For backends exposingaction_group_size/step_action_group, stages each safety-approved slot by(Action.runner_session_id, Action.tick_index)and commits one sim step only when the complete tick is present (atomicity); a different key arriving with an incomplete group drops that group and raisesROSRuntimeError, but stages the exposing slot as the new tick (asSlotGroupStagerdoes) so one lost slot costs one tick rather than wedging every later tick, and so does a replay (TickWatermark.check: the same session at or belowlast_committed_tick, a superseded session, or a session-less tickrefuse_stale_tickrefuses; a new session is adopted only when its group commits).estop()keeps the watermark;connect()clears it. Latches the group's BODY_TWIST slot into_last_body_twiston commit.read_policy_state() -> list[float] | None(L2411) — Cached simulator-native checkpoint proprioception (e.g. BEHAVIOR's 61-D R1Pro vector) for/openral/policy_state; only from an explicitobs["policy_state"], never inferred.reset_estop() -> None(L2407) — Clear the e-stop latch; caller asserts the cause has been resolved.idle_step() -> bool— Sim-only free-running stepper: advances theSimRolloutone HOLD tick (np.zeros(env_action_dim)) so cameras keep rendering when idle. ReturnsFalsewhen not connected, estop-latched, or no live MuJoCo handles. Defined only onSimAttachedHAL— real HALs never define it, since a zero vector is HOLD in sim but "drive to 0 rad" on a real arm. (L1578) Refuses to interleave a HOLD inside a mid-flight atomic action group, discarding a stalled pending group after 5 s. A step it runs zeroesbase_twist: the HOLD leaves the base still, so/odomstops reporting the last commanded twist.read_images() -> dict[str, Any]— Return latest rendered camera frames keyed by camera name from the cached_last_obs. (L2188)read_depth_clouds() -> dict[str, NDArray](L2236) — Per-depth-sensor(N,3)base_linkpoint clouds from_last_obs["depth_points"](a non-MuJoCo backend like Isaac deprojects them, so the HAL never re-derives geometry);{}when the backend renders no depth.SimSensorBridgepublishes them asPointCloud2for octomap.read_image_times() -> dict[str, int](L2217) — Sim time (ns) each cached camera frame shows, keyed likeread_images(depth cameras by sensor name), from a backend's"image_time_ns"obs slot plus the cross-reset offset, sosim_time_ns() - read_image_times()[k]is that frame's age. The Isaac sidecar reports it because its RTX pipeline delivers frames steps behind the physics (issue #361);{}when the backend reports none.SimSensorBridgestamps each frame with it.read_depth_frames() -> dict[str, dict[str, Any]](L2260) — The newest registered colour + metric depth frame per depth sensor from a non-MuJoCo backend's"depth_frames"obs slot ({name: {"depth", "rgb", "k", "optical_in_base"}}, the Isaac manifest scene's_depth_frames), kept across the steps that carry none (_keep_depth_frames);{}when the backend renders none.SimSensorBridge._publish_depth_framepublishes each frame once.update_attached_objects(objects: list[AttachedCollisionObject]) -> None(L877) — Atomically resolves a complete sim attachment snapshot fromevidence_ref="mujoco_body:<name>", including descendant bodies; preserves the previous snapshot on malformed/unknown identities. Without MuJoCo handles (Isaac) it records the set and masks nothing — the octomap bridge clears the payload by its pose, as on real hardware (#349).read_attached_objects() -> list[AttachedCollisionObject](L936) — Stable payload stateSimSensorBridgeunions with the robot self-filter so carried objects are absent from world occupancy.read_attached_body_ids() -> frozenset[int](L940) — Exact MuJoCo body ids of everything currently attached.add_post_step_observer(observer: Callable[[], None]) -> None(L944) — Registers an idempotent synchronous hook after each real simulator transition; automatic attachment evidence uses this so grasp/release changes publish before the next policy tick is acknowledged.read_scan() -> NDArray | None(L2274) — 2-D LaserScan range fan (base_link) from_last_obs["scan"]on a non-MuJoCo backend that ray-casts a lidar (Isaac);Noneotherwise. Read bySimSensorBridge._compute_scan_rangesfor/scan.mujoco_handles() -> tuple[Any, Any] | None— Forward the env's(model, data)MJCF handles. (L2044)task_success() -> bool | None(L1331) — Forwards the wrapped rollout's optionaltask_success()predicate (RoboCasa: the task's own_check_success());Nonemeans "no predicate, or one that raised", never "failed". Observability only — feeds no termination/reset/reward/action path. Logged assim.task_success(both edges, since RoboCasa success isn't latched) andsim.task_success_finalat disconnect. Full signal table indocs/reference/telemetry.md.last_committed_tick -> int[@property] (L2068) — Most recent complete atomic action group committed to the simulator.last_committed_session -> int[@property] (L2073) —runner_session_idof that group (0 = none / session-less).last_applied_action -> Action | None[@property] (L2056) — The command the simulator was last stepped with: for a slot group,compose_slot_group_actionover the manifest joints, set in step withlast_committed_tick(private_commit_group), or, when the group is not one joint-position command (BODY_TWIST / Cartesian slot, uncommanded joints, a joint slot withoutjoint_names), its gripper targets as a compact action (gripper_targets_action;Nonewhen it set none placeable); for an ungrouped action, the action itself; cleared onconnect. Fed to the grasp trigger by the lifecycle node (it had none, so slot-group jaw commands never reached it).sim_time_ns() -> int | None(L2108) — Cross-reconnect-monotonic elapsed sim time in ns;connectfolds elapsed time into an offset so a lifecycle reset doesn't rewind it.clock_authority() -> ClockAuthority(L2133) — ReturnsClockAuthority.simulation(<backend>, timestep_s=<the env's declared sim_dt_per_tick_s, else body_twist_dt_s>)whensim_time_ns()is live, elseClockAuthority.host_wall().discard_staged_slots() -> int(L1054) — Drops a half-staged action group (watermark kept); called by the HAL lifecycle node at every/openral/estoplatch and/openral/estop_cleared, so a tick a kernel stop cut short never reaches the next goal as an incomplete group. Returns the slots dropped.estop() -> None— Latch e-stop; subsequentsend_actioncalls are dropped. (L2036)- (property)
env -> SimRollout(L2168) — Direct access to the wrapped sim env (for tests / advanced wiring). - (property)
estop_latched -> bool(L2173) base_pose -> tuple[float, float, float][@property] — Current(x, y, yaw), from MJCF qpos on a MuJoCo backend orobs["base_pose"]on Isaac;(0,0,0)if the backend reports neither. Feeds the/odompublisher. (L2291)base_twist -> tuple[float, float, float, float, float, float][@property] — Last commanded body twist(vx, vy, vz, wx, wy, wz); zeroed by a non-BODY_TWIST action or an idle HOLD step. (L2344)_apply_body_twist_via_env_step(row: list[float]) -> None— Non-MuJoCoBODY_TWIST: validates the planar twist, latches it for/odom, packs(vx, vy, wz)into the scene's final base-twist slots, zeros arm/gripper, and steps.base_pose_6dof() -> tuple[...] | None— Full 6-DoF(xyz, quat_xyzw)from robocasaraw_proprio;Nonefor non-robocasa backends. (L2354)last_action_ns -> int[@property] — Monotonic ns of the last real action throughsend_action;0until the first one. Read viashould_idle_stepto yield the idle stepper to an active skill. (L2178)
python/hal/src/openral_hal/convex_distance.py
Certified signed distance between two MuJoCo convex geoms — replaces mujoco.mj_geomDistance, which under mujoco 3.8.0 returns confidently wrong values (silently) on some geom pairs the collision-evidence probes adjudicate. Represents each geom as conv(core) ⊕ ball(radius), solving via GJK (separated cores) or exact SAT (overlapping), with curved geoms bracketed by inscribed/circumscribed polytopes; runs only in evidence collection (~2-4 ms/pair), never on the 100 Hz path.
ConvexDistance[@dataclass(frozen=True)] — One certified signed distance with its proof:distance_m(the closest end of the bracket — never overstates separation; negative is penetration depth),lower_m/upper_m,certified,uncertified_reason,witness_a/witness_b,direction,duality_gap_m,method.direction(unitb→a, or the MTD axis when overlapping) is the only reliable contact direction at a flush contact, where the two witnesses coincide.as_record()serialises non-finite bounds asNone, neverInfinity, for strict-JSON round-tripping. (L93)ConvexDistance.as_record() -> dict[str, object](L140) — see above.- module const
_ARC_SEGMENTS: Final[int] = 256(L78) — default polygonisation segment count for a round-type (cylinder/ellipsoid) bracket. - module const
_BRACKET_TOL_M: Final[float] = 1e-4(L80) — certify a bracket this tight or tighter. - module const
_DUALITY_TOL_M: Final[float] = 1e-9(L82) — certify a separating-axis duality gap this tight or tighter. - module const
_OVERLAP_TOL_M: Final[float] = 1e-12(L86) — below this, GJK's terminal simplex has collapsed onto the origin (overlap is SAT's branch, not GJK's). - module const
_MAX_SAT_AXES: Final[int] = 8_000_000(L89) — refuse an exact penetration depth beyond this many SAT axes rather than spend unbounded time or silently subsample. ConvexBody[@dataclass(frozen=True)] — A geom asconv(core) ⊕ ball(radius)plus thefacesSAT needs;facesisNonefor a visual mesh (no compiled hull graph), costing nothing until the cores actually overlap. (L167)convex_geom_distance(model, data, geom_a: int, geom_b: int, *, distmax_m: float | None = None, arc_segments: int = 256) -> ConvexDistance— The instrument.distmax_menables a certified window rejection (method="beyond-window") via a separating-axis bound valid in every direction.certifiedisFalse, with a reason, for a geom with no bounded convex hull, a round-type bracket wider than 0.1 mm, or an unclosed/oversized-axis-set overlap; no plausible number is ever substituted for a defensible one. (L586)witness_clearance_m(model, data, geom_id: int, point, *, arc_segments: int = 256) -> float— Certified lower bound on how farpointlies outsidegeom_id; positive proves it's outside. Used to contradict a claimed nearest-point witness (both ends of a real nearest-pair segment lie on their own geoms). Falls back to the bounding sphere for a geom with no hull,-inffor one with neither. (L724)separating_axis_bound(points_a, points_b, direction) -> float— Weak-duality lower bound on the distance along one direction:min_B u·b − max_A u·a. Evaluated at the direction a witness claims, turning a GJK answer into a proof. Returns-inffor a degenerate direction. (L449)mesh_hull(model, mesh_id: int) -> tuple[Any, Any | None]— The hull MuJoCo itself collides,(vertices, faces)in mesh-local frame, decoded from the compiled hull graph rather than recomputed.facesisNonefor a mesh with no graph — distance stays exact, only penetration depth loses its axis set. (L188)geom_convex_bracket(model, data, geom_id, *, arc_segments=256) -> tuple[ConvexBody, ConvexBody] | None— World-frame(inscribed, circumscribed)ConvexBodypair bracketing one geom (exact for box/mesh/sphere/capsule, polygonised for cylinder/ellipsoid);Nonefor a geom with no bounded convex hull. (L280)
python/hal/src/openral_hal/sim_sensor_bridge.py
Shared sim-sensor + viewer bridge for scene-attached HAL lifecycle nodes: republishes RGB camera frames, /scan, depth PointCloud2 (MuJoCo ray-cast, or a non-MuJoCo backend's own clouds), a live MuJoCo viewer, and the sim-only idle stepper for any manifest-driven node. Also owns the attachment-state, ADR-0097 place-declaration, and E-stop ground-truth snapshot wiring; a place declaration naming a target absent from the scene is refused and logged, and its region/geometry is always this producer's own measurement, never a relayed value. rclpy imported lazily.
frame_stamp_ns(node_now_ns: int, frame_time_ns: int | None, sim_now_ns: int | None) -> int(L237) — Header stamp for a sim sensor frame: the node's now minus the frame's age on the sim clock (sim_now_ns - frame_time_ns), by the same rule asjoint_state_republish_stamp_ns(no frame time, no sim clock, or a negative / >5 s age keeps the node's now). Used bySimSensorBridgefor the RGB images, depth clouds and RGB-D frames of a backend that reportsread_image_times(issue #361).should_idle_step(now_ns: int, last_action_ns: int, idle_hold_ns: int, *, step_while_active=False) -> bool— Pure predicate for the sim-only wall-time stepper: scene-attached envs returnTrueonly after the idle-hold window; bareMujocoArmHALpassesstep_while_active=Trueso its target keeps integrating during active skills. Used bySimSensorBridge._idle_step_tick. (L152)- module const
_THUMB_INTERVAL_NS = 1_000_000_000(L37) — throttle dashboard thumbnail emission to ~1 Hz per camera; the live ROS topic itself stays atcamera_rate_hz. - module const
_IMAGE_DIM = 3(L38) — HWC ndarray rank. - module const
_RGB_CHANNELS = 3(L42) - module const
_MAX_PREATTACH_CELLS = 2_000_000(L46) — cap on the pre-attach occupancy snapshot; an over-large grid is refused/truncated rather than partially sampled. - module const
_DEGENERATE_QUAT_NORM = 1e-12(L50) — below this a quaternion carries no usable direction; keep identity rather than divide by ~0. - module const
_NEAREST_PROBE_DISTMAX_M = 0.10(L64) — near-miss probe bounding-sphere prefilter gap for the e-stop ground-truth snapshot. - module const
_NEAREST_PROBE_MAX_CALLS = 4096(L65) — exact-distance call budget for one snapshot's near-miss probes. - module const
_NEAREST_PROBE_MAX_PAIRS = 32(L66) — closest-pairs cap reported per probe. - module const
_VOXEL_BACKING_RAYS_PER_AXIS = 3(L73) — rays per cube axis forvoxel_backing_record's ray-fan sampling. - module const
_QUAT_NORM_TOL = 1e-6(L77) — tolerance on|q|^2for a published grid orientation; wide enough for a wire round-trip, too tight for an unset all-zero field. - module const
_VOXEL_BACKING_EPS_FRACTION = 0.01(L81) — ray start stand-off outside a voxel cube face, as a fraction of cell size. - module const
_VOXEL_BACKING_MAX_GEOMS = 8(L84) — cap on backing geoms reported per cell. - module const
_XYZ = 3(L86) — Cartesian dimension count for a grid size / origin / half-extent triple. - module const
_CONTACTS_CAVEAT(L91) — therobot_world_contacts==0 does NOT mean no interpenetrationstring emitted verbatim on everyestop_ground_truth_snapshotstop line. - module const
_CANDIDATE_CHUNK_HISTORY = 3(L107) — candidate chunks retained (ring buffer) for predicted-horizon reconstruction. - module const
_ESTOP_EVIDENCE_WINDOW_NS = 500_000_000(L110) — aCollisionEvidenceolder than this is not attributed to the stop being snapshotted. - module const
_DOP_VERTEX_EPS = 1e-9(L979) — slack (metres/determinant) for the 3x3 solves behind_dop_vertices. - module const
_VOXEL_BACKING_MAX_LAYERS = 4(L1270) — how many non-collidable shells one ray looks past inside a single cell before giving up. constant_scan_no_hit_ranges(*, n_beams: int, max_range_m: float) -> list[float]— Pure synthetic/scanfan: every beam clamped tomax_range_m("no hit everywhere"), the honest reading for an in-process digital twin with no scene to ray-cast. Used bySimSensorBridge._compute_scan_ranges's no-handles branch. (L136)estop_ground_truth_snapshot(model, data, *, robot_body_ids: frozenset[int], attached_body_ids: frozenset[int] = frozenset(), probe_body_ids: frozenset[int] | None = None, base_frame_body: str | None = None, joint_state: Any = None, description: Any = None, evidence_voxel: Any = None, distmax_m: float = 0.10, max_pairs: int = 32, max_calls: int = 4096) -> dict[str, object]— Pure MuJoCo ground truth for one safety stop (attached-payload or robot-world): contacts, poses, and near-miss probe pairs across robot↔world, payload↔world, payload↔robot-link, and link↔link. Contact lists are not a penetration oracle — contype/conaffinity exclusions can suppress a real contact entirely — so the near-miss probes are the adjudicator, measured viaconvex_geom_distance(nevermujoco.mj_geomDistance, which returns confidently wrong values on some pair classes under mujoco 3.8.0). Every probe side is filtered to solid geometry only (a geom with neithercontypenorconaffinityis never a real penetration) and reports its own coverage (candidate/probed pairs, certified/uncertified, noncollidable exclusions) so an absent pair reads as "looked and found nothing," not silence.descriptionadds anadjudication_budget(mesh-vs-model slop) so a kernel OBB↔voxel distance and a probe mesh↔mesh distance can be compared fairly, widening (never narrowing) the probe window to the admissible gap;evidence_voxeladdsevidence_voxel_backing(the only part of this record that reads the published map rather than MuJoCo alone) and must only be passed for evidence judged fresh. Called bySimSensorBridge._on_estop_ground_truth. (L2066)kernel_checked_body_ids(model, description) -> frozenset[int]— Pure MJCF bodies for exactly the links the safety kernel collision-checks (the manifest'scollision_geometryis the kernel's model, so a link with no entry is deliberately invisible). Resolved exactly via each joint'ssim_joint_name, never name-mangled; empty when the manifest declares no collision geometry. Scopes the near-miss probes so a mobile base's floor contacts can't bury the arm's. Sibling ofdepth_cloud.robot_self_body_ids(that answers "what is the robot"; this answers "what does the kernel check"). (L705)kernel_checked_link_bodies(model, description) -> dict[str, int]— Pure name-keyed form ofkernel_checked_body_ids, resolving eachcollision_geometrylink name to its MJCF body id the same exact way. Needed wherever a diagnostic must line a manifest OBB up with its MuJoCo geometry (collision_model_mesh_slopis the caller); unresolved links are reported, not guessed. (L730)collision_model_mesh_slop(model, description) -> dict[str, object]— Pure measurement of how far the kernel's manifest-OBB collision model reaches beyond the real meshes per link, so a kernel distance (OBB↔voxel) and a probe distance (mesh↔mesh) can be compared fairly — the admissible gap iscorner_slop(link) + voxel_half_diagonal. Returns per-linkobb_half_extents_m/face_slop_m/corner_slop_m/hull_overhang_m(has_stage2_hulldistinguishes "no hull" from "unmeasured") plusmax_corner_slop_m;{}when the manifest declares no collision geometry, so the caller reports "no budget" rather than assuming zero. Robot side only — an attached-payload self stop also needsattached_payload_mesh_slop. (L807)_dop_vertices(dop_lo, dop_hi) -> NDArray[np.float64]— Vertices of a 26-DOP from its 26 halfspaces, by direct plane-triple enumeration (no hull-intersection library). Needed because distance-to-a-convex-set is a convex function, so its maximum over a polytope occurs at a vertex — the same reason a box's slop term is its corner, not something face-sampled._payload_model_overhang_m(prim, points) -> float— How far one payload primitive's kernel model (its DOP where one ships, else the box) reaches past the real mesh — the payload-side analogue of a link'shull_overhang_m, used by a world-voxel payload stop's budget. Deliberately the DOP and not a tighter stage-2 hull even when one ships, since the kernel falls back to the DOP whenever its hull budget caps out.inffor a degenerate refinement, so the consumer reads "no budget" rather than scoring against zero.attached_payload_mesh_slop(model, data, *, attached_body_ids: frozenset[int], max_primitives: int = 16) -> dict[str, object]— Pure measurement of how far a carried payload's kernel primitives reach beyond its own meshes — the payload side of the adjudication budget an attached-payload self stop needs (that check is OBB-vs-OBB on both sides, unlike a world-voxel stop's single OBB). Callsextract_body_primitivesdirectly so the budget can't drift from the geometry the kernel was actually handed; only box primitives can be loose (sphere/capsule primitives have no corner to overhang). Returns{"objects": {<body>: {n_primitives, n_box_primitives, corner_slop_m, ...}}, "max_corner_slop_m", "unresolved_objects", "method"};{}when nothing is carried, so the caller reports "no budget" rather than assuming zero. (L1084)voxel_backing_for_cell(model, data, cell: Mapping[str, Any], *, robot_body_ids: frozenset[int], attached_body_ids: frozenset[int], base_frame_body: str | None) -> dict[str, object] | None— The single place a cached/openral/world_voxelscell dict is unpacked intovoxel_backing_record's arguments, so the in-snapshot and late-evidence call sites can't disagree about which fields (notablygrid_orientation_xyzw) they pass — omitting it silently places the probe cube by the wrong axes on a rotated base. ReturnsNonewhen the cell names no index. (L2019)voxel_backing_record(model, data, *, voxel_index: int, grid_origin: Sequence[float], grid_resolution: float, grid_size: Sequence[int], robot_body_ids: frozenset[int], attached_body_ids: frozenset[int] = frozenset(), base_frame_body: str | None = None, grid_orientation_xyzw: Sequence[float] = (0.0, 0.0, 0.0, 1.0), rays_per_axis: int = 3) -> dict[str, object]— Pure answer to what MuJoCo geometry, if any, backs one occupancy voxel — the near-miss probes never look at/openral/world_voxels, so this is the only check of whether the map and the world agree. Decodes the kernel'sb=voxel_<n>index against the live grid, lifts it to world coordinates, then classifies via ray-fan sweep assolid_world,attached_payload,self_occupancy_suspect,noncollidable_world, orunbacked, withverdictpicking by adjudication order (real world geometry always explains a stop first).rays_cast/rays_hitmakeunbackedreadable as "looked and found nothing"; an index outside the grid returnsverdict: "out_of_range". (L1480)attached_model_slip(model, data, *, description: Any, attached_objects: Sequence[Any]) -> list[dict[str, object]]— Pure answer to where the kernel thinks each carried payload is (forward-kinematics fromattach_link+pose_in_link, fixed at the attach) versus where the simulator actually has it — a payload that slips, settles, or pivots in the gripper is then charged as collision depth against cells the real object isn't near. One record per object:object_id,attach_link,resolved,kernel_world_xyz,sim_world_xyz,slip_m,rotation_deg,body_radius_m,max_point_slip_m(translation plus the rotation's sweep at the body's radius, sinceslip_malone understates a pivoting payload). An object whose link/body/pose can't be resolved reportsresolved: False, never a silent zero. Emitted on every ground-truth snapshot aspayload_model_slip. (L1776)grid_with_origin(history: Sequence[Mapping[str, object]], origin: Sequence[float], *, tol_m: float = 1e-9) -> Mapping[str, object] | None— The cached/openral/world_voxelsgeometry whose published origin matches the one the kernel discloses (CollisionEvidence.world_grid_origin_m); exact wheregrid_current_at's stamp match is ambiguous, since two grids published within one/clocktick can be indistinguishable by stamp while naming different cells. Newest match wins.Nonewhen the kernel disclosed nothing or no cached grid matches; the caller then falls back to the stamp proxy and records which mechanism resolved it (grid_decode.decode_source). (L1714)grid_current_at(history: Sequence[Mapping[str, object]], stamp_ns: int) -> Mapping[str, object] | None— The newest cached/openral/world_voxelsgeometry whose header stamp is at or beforestamp_ns, chosen by stamp, never by arrival order: decoding a stop's cell index against whichever grid arrived last is wrong the moment the base drifts across one lattice boundary, misplacingworld_xyzby a full grid resolution.Nonewhen nothing inhistoryis old enough; the caller then falls back to the latest grid and recordsfallback_to_latest: truerather than papering over the gap. (L1747)occupied_cell_keys(*, occupancy: Sequence[int] | Any, grid_origin: Sequence[float], grid_resolution: float, grid_size: Sequence[int], grid_orientation_xyzw: Sequence[float], base_rot: Any, base_offset: Any, max_cells: int = 2_000_000) -> tuple[frozenset[tuple[int, int, int]], bool]— Pure reduction of one publishedOccupancyVoxelsgrid to the set of world cells (keyed byfloor(centre_world / resolution), not rounded or by voxel index — both are wrong once the base has moved) it marks occupied, so a later stop can ask whether its cell predates a given moment (e.g. a grasp). An occupied count abovemax_cellsreturns(frozenset(), True)rather than a partial set, since a partial set would wrongly answer "not pre-existing" for cells it never looked at. (L1873)preattach_verdict(*, world_xyz: Sequence[float], resolution: float, preattach_keys: frozenset[tuple[int, int, int]] | None) -> dict[str, object]— Did the cell a stop tripped on already exist before the payload was masked? Returns{available, preexisting, within_one_cell, cell_key, preattach_cells}, or{available: False, reason}when no snapshot was taken — never a bareFalse, which would misread as "the cell is new."preexistingalone is not a payload-authored finding (world geometry is always pre-existing too); that requires the conjunction withvoxel_backing_record's verdict beingattached_payload. Consumed bySimSensorBridge._annotate_preattach. (L1961)candidate_chunk_digest(*, stamp_ns: int, control_mode: int, horizon: int, n_dof: int, flat: Sequence[float], cartesian_delta_scale: Sequence[float] = (), ee_name: str = "", frame_id: str = "", rskill_id: str = "", trace_id: str = "", tick_index: int = 0, runner_session_id: int = 0) -> dict[str, object]— Pure JSON-safe digest of oneopenral_msgs/ActionChunk, reshaped intohorizonrows ofn_dofunderticks(a mismatched length is kept verbatim withshape_mismatch: true, never truncated). Recordsrunner_session_idbesidetick_index(0 = a session-less producer), since a tick number alone is ambiguous across a runner restart. The command-side record of a predicted-horizon stop — the configuration the kernel actually adjudicated is insteadCollisionEvidence.joint_positions_rad, the authority. Cached per chunk bySimSensorBridge._on_candidate_action. (L2491)initial_configuration_stop_record(snapshot: Mapping[str, object], *, stop_seq: int, last_action_ns: int, candidate_chunks_seen: int) -> dict[str, object] | None— Pure classifier separating a scene-initialization stop from a mid-task one:Noneonce anything has been applied (last_action_ns != 0), else a record naming the stop as one the scene reset produced rather than one the robot was driven into — no policy, chunk, or margin change can clear it.candidate_chunks_seenis reported but doesn't gate, since a chunk the kernel rejected never reachedsend_action. Diagnostics only — never suppresses, delays, or alters the stop. Called bySimSensorBridge._log_initial_configuration_stop. (L2561)isaac_estop_ground_truth_record(contacts: Mapping[str, Any], *, joint_state: Any = None, evidence_voxel: Mapping[str, object] | None = None, attached_object_ids: Sequence[str] = ()) -> dict[str, object]— Pure Isaac counterpart ofestop_ground_truth_snapshot, built from the sidecarcontactsreply (_IsaacSimSidecar.contact_ground_truth):backend: "isaacsim",sim_time_s,robot_world_contacts(one record per robot-collider/other-collider pair,min_separation_mnegative = penetration),max_penetration_m,nearest_robot_world_pairs: None+nearest_unavailable_reason(PhysX has no shape-to-shape distance),contacts_caveat,robot_joint_state,evidence_voxel,attached_object_ids. No payload sections,adjudication_budgetor voxel backing (MuJoCo-only). Called bySimSensorBridge._on_estop_isaac_ground_truth. (L2435)class SimSensorBridge— Wires and tears down sim-sensor publishers and the MuJoCo viewer on a HAL lifecycle node, gated on the robot manifest + HAL capability.attachment_heartbeat=False(the HAL node sets it viasim_attachment_heartbeatwhen its vision attachment leg is on, and always underhal_mode:=real, where "nothing attached" has no evidence behind it) opens no/openral/attachment_statepublisher, timer or evidence tracker at all — whatever the HAL, including one WITH the attachment API (SimAttachedHAL, the Isaac / scene-attached path, which would otherwise keep publishing its empty revision-0 set beside the vision leg, so the leg's set and grasp/place declarations would alternate on the latched topic) — so one authority publishes on it. Its staging subscriptions (/openral/attachment_state,/openral/attachment_state_applied,/openral/world_voxels) stay for a HAL with the attachment API, so the other authority's revisions are still masked and still held on the attachment barrier (tests/integration/test_hal_attachment_barrier_live.py). Owns/scanfor both a live MJCF ray-cast (SimAttachedHAL) and aconstant_scan_no_hit_rangesfan for the bare digital twin. (L2646) Attachment heartbeat for every sim HAL (2026-09-22):_setup_attachment_state()creates the/openral/attachment_statepublisher and its 0.2 s timer for every sim HAL whenattachment_heartbeatis true (the default); only the staging path (subscriptions + the MuJoCo evidence tracker) still requires the HAL'supdate_attached_objects/read_attached_objectsAPI. The deploy launch enables the kernel's attached-payload check for every sim robot with capsules, and that check fails closed on a world state whoseattachment_stamp_nsis still 0 — so aMujocoArmHALtwin (no attach mechanics, zero publishers) had every JOINT chunk dropped asattached_overflowon the OpenArm twin. "Nothing attached, fresh" is the exact truth for such a HAL; covered bytests/integration/test_sim_sensor_bridge_attachment_heartbeat.py.- (property)
idle_hold_s -> float(L2874) — The idle stepper's quiet window in seconds (the longest joint-state gap the twin produces while a skill pauses between VLA chunks). The HAL node raises the vision leg's jaw-evidence timeout toidle_hold_s+ 4 joint-state periods on the idle-stepping sim twin only (twin_jaw_evidence_timeout_s; never underhal_mode:=real, whose node also builds this bridge) so the attachment heartbeat survives re-inference pauses. __init__(node: Any, hal: Any, description: RobotDescription, *, viewer_enabled: bool = True, camera_rate_hz: float = 10.0, viewer_sync_rate_hz: float = 30.0, scan_rate_hz: float = 10.0, scan_n_beams: int = 360, scan_max_range_m: float = 12.0, scan_min_range_m: float = 0.05, depth_rate_hz: float = 10.0, depth_max_range_m: float = 5.0, depth_pixel_stride: int = 4, idle_hold_ms: float = 2000.0, attachment_heartbeat: bool = True, on_step: Any = None, on_attachment_perception_ready: Any = None) -> None—on_steprefreshes the proprio snapshot after idle sim steps.on_attachment_perception_readyreleases a deferred grouped-action acknowledgement only once a newly attached payload is transparent in both a depth frame and a newer/openral/world_voxelsraster.setup() -> None— Activates every stream the manifest + HAL support: RGB camera +CameraInfopublishers (advertised lazily per camera on its first real frame — see_advertise_camera), the sim-only idle stepper, and the passive MuJoCo viewer (alwaysmjCAMERA_FREE, opening pose frominitial_viewer_camera; a GL/DISPLAY failure warns and continues). Idempotent per activate. (L2882)_advertise_camera(name: str) -> Any— Creates (once) and returns theImage+camera_infopublisher for one camera, on its first real rendered frame — never eagerly for the manifest's full camera list, since a manifest may declare a camera the current scene can't render and an eagerly-advertised topic would be indistinguishably silent forever. A declared camera that never yields a frame gets one warning and no topic. (L3029)_publish_depth_optical_mount(spec) -> None— Latches<mount> -> <mount>_optical_frame(BODY_TO_OPTICAL_QUAT_XYZW) on/tf_staticfor a MuJoCo depth sensor whoseframe_idis a body-convention mount with a manifest pose; its sim camera sits at that mount (SensorSpec.sim_placement), so raster, cloud and colour are placed where they were cast._mujoco_depth_frame(name, kwargs, model, data, stride) -> tuple[dict, clearing]— One MuJoCo depth camera as anopenral_sim.depth_framesframe: the frame's single ray-cast (synthesize_depth_frame, strided raster + K/stride + the self-filter's clearing mask), the same MJCF camera's colour at that raster (_render_registered_colour), and the camera's live optical pose inbase_frame(identity when no base body resolves; the static mount then carries the TF)._render_registered_colour(name, model, data, camera_name, w, h, k) -> NDArray | None— The depth camera's colour rendered at its raster from the sameMjData(renderer cached per model: RoboCasa rebuilds the model each reset).None(warned once, camera disabled) when K is not the square, centred pinhole the camera'sfovyrenders._publish_depth_frame(name, spec, frame, stamp, *, tf_stamp) -> None— Every backend's path (the MuJoCo bridge's own frames andSimAttachedHAL.read_depth_frames): one RGB-D frame → depth32FC1+ colourrgb8Images sharing oneCameraInfo(the backend's K), all indepth_optical_frame_id(spec)and stampedstamp(the time the frame shows), plusbase_frame -> <optical>on/tfstampedtf_stamp(now: the backend measured that pose at the current step, so the TF history stays true). Colour/depth size mismatch publishes depth only, warned once; an emptyrgbpublishes depth only;tf_stamp=Nonepublishes no TF._frame_times() -> tuple[dict[str, int], int | None]/_frame_stamp(now, frame_time_ns, sim_now_ns)— The HAL'sread_image_times()andsim_time_ns(), andframe_stamp_nsas a header stamp in the node's clock:_publish_images(each RGB image + itsCameraInfo) and_publish_depth_clouds_from_obs(clouds, RGB-D frames) stamp every frame with the time it shows instead of the publish time (issue #361)._colour_publishers(name) -> tuple[Publisher, Publisher]— The depth camera's colourImage+CameraInfopublishers oncamera_topic(name), created once (RGB-camera QoS).teardown() -> None— Cancel timers (incl. the idle-step timer), destroy publishers, close viewer. Called fromon_deactivate/on_cleanup. (L2893)_setup_estop_ground_truth() / _on_estop_ground_truth(msg)— Diagnostics only — nothing here gates, delays, or alters actuation. Gated on live MuJoCo handles or an Isaac contacts source (_contact_ground_truth_source: the wrapped env'scontact_ground_truth), not attachment support, so a pre-grasp arm↔world stop still gets ground truth. Subscribes/openral/estop,/openral/candidate_action(a ring of cachedcandidate_chunk_digests),/openral/failure/safety(the kernel'sCollisionEvidence), and the/openral/world_voxelsgrid geometry, then emits onesim.estop_ground_truth_snapshotline per stop plus a separatesim.estop_ground_truth_evidenceline when the kernel's evidence lands late (never delayed for it, since sim state must be captured at the stop instant). Also runs_log_initial_configuration_stop, emittingsim.estop_initial_configurationwhen nothing has been applied yet. Without MuJoCo handles the stop goes to_on_estop_isaac_ground_truth: the same line fromisaac_estop_ground_truth_record(a sidecarROSErrorlands incontacts_error, never raised). Full signal table indocs/reference/telemetry.md.attachment_action_ack_ready() -> bool(L4537) — Attachment transaction gate for the lifecycle node: an addition stays unacknowledged until kernel acceptance, one transparent depth frame, and the following voxel raster all land. An empty detach snapshot is immediately ready, since detach unmasks before the kernel drops the old payload._on_attachment_state_applied(msg)— Applies the staged revision once the kernel accepts it, then arms the perception barrier only when the revision masks geometry not already masked (compared exactly by(object_id, evidence_ref)); a revision masking nothing new (e.g. a re-published place witness) releases the barrier immediately instead of deadlocking a successful place.describes_mobile_base(description: RobotDescription) -> bool(mobile_base_bridge.pyL40) — Pure predicate: does the manifest declare a planar base (base_joints)? The single answer to "does something already ownodom → base_frame", soMobileBaseBridge's attach andSimSensorBridge's TF skip-guard can't drift apart; reading any other field risks a secondbase_linkparent and a split/tftree.class MobileBaseBridge(mobile_base_bridge.pyL66) — Generic planar-mobile-base ROS wiring (sibling ofSimSensorBridge): owns/odom, theodom->base_linkTF, and the/cmd_vel→BODY_TWIST bridge (bypasses the safety supervisor — out of scope here). Frame ids come fromRobotDescription;ManifestHALLifecycleNodeattaches it inon_activate_post_subsiff the manifest declaresbase_joints.__init__(node, hal, description, *, odom_rate_hz: float = 20.0, cmd_vel_topic: str = "/cmd_vel", proprio: Any = None) -> None—proprio: when set (sim-attached HALs), odom is published from the node's dedicated thread viapublish_from_snapshot, reading the snapshot not the simulator;None(real HALs) keeps the legacy odom timer.setup() -> None(mobile_base_bridge.pyL109) — Create the/odompublisher + TF broadcaster +/cmd_velsubscription; the odom timer is created only whenproprio is None(sim HALs publish odom off the node's thread).publish_from_snapshot() -> None(mobile_base_bridge.pyL163) — Dedicated-thread entry: publish one/odom+ TF sample from the proprio snapshot (never the simulator). Thin alias over_publish_odom(which branches onproprio).teardown() -> None(mobile_base_bridge.pyL150) — Cancel the timer (if any) + destroy the publisher/subscription. Idempotent.
python/hal/src/openral_hal/proprio_snapshot.py
Decouples the control-critical publishers (odom / joint_state / TF) from the single executor thread that runs env.step + render + raycast. The sim-attached HAL node captures a frame after each step (on the executor thread, where reading the sim is safe) and a dedicated publisher thread re-emits it at ~30 Hz, so odom stays fresh (~28 Hz live, vs ~1.8 Hz starved) without ever touching MjData/GL off the executor thread (a MultiThreadedExecutor was rejected — MuJoCo's GL context is thread-affine).
class ProprioFrame(L38) — Frozen dataclass: one coherent proprio sample (state,base_pose,base_pose_6dof,base_twist,sim_time_ns,policy_state). Plain immutable data, no live simulator handles, so it's safe to publish from a different thread than the one that stepped the sim.class ProprioSnapshot(L66) — Lock-guarded holder for the latestProprioFrame: the executor thread callssetafter each step, the publisher thread callslatest; the frame is swapped under the lock so a reader never sees a torn frame or reaches the HAL directly.set(frame: ProprioFrame) -> None(L94) — Atomically publishframeas the latest sample (executor thread only).latest() -> ProprioFrame | None(L99) — Return the most recent frame, orNonebefore the first capture.
python/hal/src/openral_hal/_sim_attachment_evidence.py
MuJoCo attachment evidence — the counterpart of _vision_attachment_evidence.py, emitting the identical AttachedCollisionObject contract. Only the contact-force and payload-lowering halves are inventoried here; support-contact and place-region predate this section.
- module const
_LOGGER(L39) — module structlog logger. - module const
_DEGENERATE_NORM = 1e-9(L43) — below this a summed support normal carries no direction; used bysupport_contact_witness. - module const
_SUPPORT_PROBE_GAP_M = 0.001(L58) — beyond this gap nothing is resting on anything (the safety kernel's ownattached_contact_tolerance_m). - module const
_SUPPORT_MAX_PENETRATION_M = 0.01(L64) — the safety kernel'ssupport_witness_max_penetration_m; a claim past this fails the whole attachment message, so the producer never constructs one. - module const
_SUPPORT_MAX_PATCH_RADIUS_M = 0.5(L65) — the safety kernel'ssupport_witness_max_patch_radius_mmirrored here. - module const
_SUPPORT_PROBE_MAX_CALLS = 1024(L75) — exact-distance call budget for one support attestation. - module const
_CONTACT_FORCE_SCALE_ENV = "OPENRAL_SIM_CONTACT_FORCE_N_PER_UNIT"(L88) — ADR-0100 contact-force scale env var read bycontact_force_calibration. - module const
_CONTACT_FORCE_CALIBRATION_REF_ENV = "OPENRAL_SIM_CONTACT_FORCE_CALIBRATION_REF"(L89) — companion calibration-reference env var; both must be set forcalibrated=True. - module const
_CONTACT_FORCE_DEFAULT_SCALE = 1.0(L90) — identity scale used when calibration env vars are unset. - module const
_NORMAL_AGREEMENT_MIN = 0.5(L97) — minimum cosine agreement between a surface normal and the probe's own contact direction before a support hit is attested. class SimObjectMobility(str, Enum)(L100) — Kinematic mobility of a contacted non-robot MuJoCo body:FREE,HINGE,SLIDE,ARTICULATED,FIXED. Returned byclassify_body_mobility.subtree_region_box(model, data, *, root_body_id) -> tuple[NDArray, NDArray] | None(L198) — Sim producer for a declared place target'sPlaceRegion: bounds one body's whole collision subtree by a box axis-aligned in the declared body's own frame, computed once at declaration time from the model, never inferred from the payload's position. Returns(centre_in_body_frame, half_extents), orNonewhen the subtree has no (or degenerate) collision geometry.lift_region_off_support(region_local, rotation_base_from_body, *, resolution) -> tuple[NDArray, NDArray] | None(L245) — The sim mirror of_grasp_target.target_region_from_mask's HZ-0115-6 rule: shrinks the body-frame box's face on the body axis closest to base+zbyresolution / |cos tilt|, so the box's lowest point rises exactly one voxel off the support (the target's own lowest point in sim) and the support cells stay outside the grasp region.Nonewhen the target is no taller than one voxel (the realNO_HEIGHT_ABOVE_SUPPORT). Kept local, not shared with_grasp_target, because the real producer fits a gravity-aligned box from a cloud.classify_body_mobility(model, body_id) -> SimObjectMobility(L309) — Classifies the contacted body's root asFIXED(no joint),FREE,HINGE/SLIDE(all one joint type), orARTICULATED(mixed).geom_is_polytope(model, geom) -> bool(L413) — Isconv(geom_surface_points(geom))the geom's own solid? True for mesh/box, false otherwise: for a curved surface (e.g. a sphere sampled at its poles) that containment runs the wrong way and can under-measure a payload's real extent past a voxel's half-diagonal. Gates both lowering paths; in_clustered_box_primitivesone ungroundable geom disqualifies the whole cluster.geom_surface_points(model, geom) -> NDArray[np.float64](L459) — Points lying on one geom's own surface in its local frame (mesh vertices; box corners; sphere/capsule poles); empty for geometry with no enumerable surface (plane, heightfield, SDF). Deliberately not a bounding cube, which would overstate the geometry and understate the slop both callers measure._tight_geometry_from_points(points, half_extents) -> TightCollisionGeometry | None— Refines one payload box into the kernel's staged 26-DOP + exact hull online at attach (the payload twin oftools/generate_tight_geometry.derive_tight_geometry), so thatmesh ⊆ hull ⊆ DOP ⊆ boxholds exactly. No convex-hull solver runs — stage 2 is a support scan over the deduplicated points. OverMAX_TIGHT_HULL_VERTICESit returns the DOP alone, disclosed by an empty vertex tuple, never a truncated hull.Noneonly when there are fewer than four surface points to refine.- module const
_HULL_DEDUP_CEILING = 2 * MAX_TIGHT_HULL_VERTICES(L522) — bounds the sort_tight_geometry_from_pointsruns when deduplicating hull vertices. extract_body_primitives(model, data, *, root_body_id, object_id, max_primitives=16) -> list[AttachedCollisionPrimitive](L871) — Extracts conservative collision primitives for one unknown sim object: one per collision geom in the body's subtree, or_clustered_box_primitiveswhen there are more geoms thanmax_primitives. Called bycollision_model_mesh_slop/attached_payload_mesh_slopto keep their slop budget from drifting off the kernel's actual geometry. RaisesROSConfigErrorwhen the subtree has no collision geometry.support_contact_witness(model, data, *, root_body_id, robot_body_ids, stamp_ns, support_roots=None) -> SupportContactWitness | None(L1258) — Attests one payload's bounded support contact from certified signed distances (openral_hal.convex_distance, not the solver's contact list ormj_geomDistance), so a grasped object resting on the counter it was picked from isn't indistinguishable from driving it through a wall. Only a non-free environment body counts as a support; the place phase restrictssupport_rootsto the declared target's own subtree.Nonewhen no eligible support contact exists.contact_force_calibration() -> tuple[float, bool, str | None](L1388) — Resolves the sim contact-force calibration fromOPENRAL_SIM_CONTACT_FORCE_N_PER_UNIT+OPENRAL_SIM_CONTACT_FORCE_CALIBRATION_REF;calibrated=Trueonly when both are set and the scale parses finite and positive — a sim magnitude is a calibration knob, not a newtons claim, and a scale nobody can name must not silently arm the kernel's force gate.probe_contact_force(model, data, *, payload_geoms, target_root_body, target_name, stamp_ns) -> ContactForceWitness | None(L1432) — Walks MuJoCo's solver contact list for payload↔declared-target pairs and attests the total normal load viamj_contactForce(never just the largest single contact, which understates a multi-corner rest).Noneis not evidence of absent contact — contype/conaffinity exclusions can suppress a pair entirely — it only means the gate does not arm; that blind spot is survivable because this check can only add a refusal, never remove one.class SimAttachmentEvidenceTracker(model, description, *, stable_ticks=3, release_ticks=3, translation_tolerance_m=0.01, rotation_tolerance_rad=0.15)(L1554) — Confirms free-object grasp/release from exact MuJoCo contacts and motion; fed every physics step bySimSensorBridge. Resolves the manifest's gripper-role joints to one attach link + touch links at construction; raisesROSConfigErrorfor none, several parent links, or too few contact branches for the end effector'skind.update(data, *, stamp_ns) -> list[AttachedCollisionObject] | None(L1690) — Returns a new complete attachment set only when the attachment changes (attach, detach, or a place-witness arm/disarm);Noneotherwise, so a heartbeat never re-arms an exemption the kernel had killed.set_place_declaration(declaration: PlaceDeclaration | None) -> None(L1866) — Installs, replaces, or retracts the active place-phase declaration; resolvestarget_idto a MuJoCo body at install time, so an unresolvable target is refused (ROSConfigError) rather than silently no-op'd per tick.place_declaration(data, *, stamp_ns) -> PlaceDeclaration | None(L1929) — The live declaration, with its region and geometry posed into the robot base frame; overwrites an incoming region/geometry it can measure and drops what it cannot, so a value a dispatcher relayed can never reach the kernel wearing this producer's authority.set_grasp_declaration(declaration: GraspDeclaration | None) -> None(L2053) — The grasp mirror ofset_place_declaration: resolvestarget_id(sim:<body>) to a MuJoCo body at install time, refusing an unresolvable one withROSConfigError(the previous grasp state is cleared either way);Noneor a retracted declaration clears it. Touches nothing else in the tracker.set_voxel_resolution(resolution_m: float) -> None(L2138) — Record the occupancy lattice's cell edge for the grasp region's one-voxel lift;SimSensorBridge._fill_grasp_declarationpasses the last/openral/world_voxelsgrid'sresolution. Not finite and positive →ROSConfigError.grasp_declaration(data, *, stamp_ns) -> GraspDeclaration | None(L2091) — The live grasp declaration with its region measured from the target's model subtree and posed in the base frame — box only, nogeometry(v1). Its lower face is lifted one voxel off the support (lift_region_off_support), so no region is published untilset_voxel_resolutionhas been called (fail closed; an attach before that freezes the region-less state for that declaration). Re-validated againstGraspDeclaration's caps (a receptacle-sized box yieldsregion=None); an incoming region is always replaced or dropped. Re-measured at the target's live pose only until the declared object (object_id, empty = any) attaches; from that edge the region is frozen at the attach-instant measurement (itsstamp_nskept) through carry and release, so the kernel's handover rule can fire — only a newset_grasp_declarationclears the freeze.Noneonce retracted or pasttimeout_s.SimSensorBridgesubscribes/openral/grasp_declaration(RELIABLE + TRANSIENT_LOCAL, depth 1) only where this tracker exists and fills it onto everyAttachmentStateenvelope without bumping the revision; a twin without the attachment API relays no declaration.
python/hal/src/openral_hal/_vision_attachment_evidence.py
Real-hardware attachment evidence from a segmenter mask plus wrist depth — the counterpart of _sim_attachment_evidence.py, emitting the identical AttachedCollisionObject contract so the safety kernel cannot tell the two producers apart. Every fail-closed gate is geometric; the segmenter's own confidence is recorded but never thresholded. Scoped to free objects only — vision gives geometry, not kinematic class.
- module const
_MIN_CLOUD_POINTS = 24(L97) — minimum points a cloud needs before a PCA orientation means anything; below thisclustered_obb_primitivesstays axis-aligned. - module const
_BOX_EPSILON_M = 1e-4(L101) — padding added to every fitted box half-extent, matching the sim producer's own epsilon so neither producer emits a zero-thickness slab. - module const
_VISION_CONFIDENCE = 0.7(L107) — confidence stamped on an accepted vision attachment (below the sim producer's 1.0 since this one infers geometry from one partially-occluded viewpoint). Calibration point, not measured. - module const
_FALLBACK_CONFIDENCE = 0.2(L112) — confidence stamped on the conservativejaw_span_primitivefallback box. Calibration point, not measured. class VisionGateConfig— Frozen dataclass of gate thresholds:containment_radius_m=0.12,max_payload_extents_m=(0.10, 0.25, 0.25),max_payload_volume_m3=0.004,min_depth_valid_fraction=0.30,min_depth_m=0.02,max_depth_m=1.0,view_ray_inflation_m=0.02,jaw_span_m=0.05,max_primitives=16. Every value is a chosen calibration point, not a measured constant; the extent/jaw-span caps aren't derivable from the manifest today (EndEffectorSpechas no jaw-aperture field). (L116)class VisionAttachmentReport— Frozen dataclass carrying what the gates measured, for the trace:accepted,rejections("no_candidates"/"empty_mask"/"depth_validity"/"payload_extent"/"payload_volume"/"jaw_containment"/"no_valid_depth"),depth_valid_fraction,point_count,extents_m,volume_m3,centroid_distance_m,candidate_index/candidate_count,mask_score_advisory(recorded only, never thresholded or used to select). (L185)backproject_masked_depth(mask, depth_m, intrinsics, *, min_depth_m, max_depth_m) -> tuple[NDArray, float]— Back-projects a mask's pixels through a metric depth frame into the camera optical frame (REP-103), returning(N, 3)points and the fraction of masked pixels with usable depth. An empty mask returns((0, 3), 0.0)— not an error. RaisesROSConfigErroron shape disagreement or mismatched-resolution intrinsics. (L227)clustered_obb_primitives(points_in_link, *, object_id, max_primitives=16, inflation_m=0.0, inflation_axis_in_link=None) -> tuple[list[AttachedCollisionPrimitive], NDArray, NDArray]— Reduces a payload cloud to bounded oriented boxes, the same reduction as the sim producer's_clustered_box_primitivesbut split in the cloud's PCA frame so boxes orient to the object.inflation_axis_in_linkpads each axis by its share of the view ray, compensating for the unobserved far side. Clouds under 24 points stay axis-aligned rather than orienting off noise. (L317)palm_plane_m(tcp_in_link, palm_in_link) -> float(L407) — How far along the attach-link-origin → tool-point approach the palm plane (the jaw joint's origin) lies; 0 at or behind the origin. A held object never lies behind it: the producer drops mask points behind it (behind_palmwhen none are left), poses the jaw-span fallback cube centred on the tool point but no further back than the palm plane, and the bridge clips every payload there (#349).VisionAttachmentEvidenceProducer.palm_in_linkis the jaw joint's origin.jaw_span_primitive(*, object_id, jaw_span_m) -> AttachedCollisionPrimitive— The conservative fallback box: a cube of half-extentjaw_span_mcentred on the TCP, deliberately crude so a rejected mask still leaves geometry in front of the collision checker. (L432)class VisionAttachmentEvidenceProducer(description, *, gripper_joint=None, config=None)— Event-driven producer (no per-frameupdate, unlike the ticked sim tracker). Withgripper_jointset it serves that one hand:attach_linkis itsparent_link,touch_linksitschild_link(one producer per hand on a bimanual robot). WithNoneit serves every gripper-role joint, which must share one parent link. RaisesROSConfigErrorwhen there are none, the name is not a gripper joint, or (unnamed) they don't share one parent. (L469)on_grasp(*, masks, depth_m, intrinsics, t_link_from_cam, tcp_in_link, object_id, stamp_ns, mask_scores_advisory=()) -> tuple[AttachedCollisionObject, VisionAttachmentReport]— Fits and gates one payload from theSegmentInViewreply's candidate masks; among candidates clearing every gate, the largest fitted volume wins (over-approximation is the conservative error), never the model's own score. Always returns an attachment: any rejection degrades to thejaw_span_primitivefallback at low confidence, neverNone. An accepted mask is stampedVISION_SEGMENTATION;mass_kg/center_of_mass_m/inertia_kg_m2stayNone(vision gives geometry only). (L679)on_release() -> list[AttachedCollisionObject]— Detach: an empty list, a complete attachment set meaning "nothing is held", mirroring the sim producer's release path. (L562)- (property)
attach_link -> str(L543) - (property)
touch_links -> tuple[str, ...](L553) — finger links allowed to touch the payload: the jaws and their mimic followers. - (property)
palm_in_link -> tuple[float, float, float](L548) — the jaw joint's origin in the attach link (palm_plane_m). - (property)
config -> VisionGateConfig(L558)
Not covered (see the module docstring): articulated objects, support contact, transparent/thin objects, self-occlusion, mass/CoM/inertia, deformables, multi-object grasps, and the attach trigger itself.
python/hal/src/openral_hal/_grasp_target.py
Pre-grasp target geometry (design note docs/reference/real-pick-place-design.md §2.2): seed from the voxel map, SAM prompt projection, a measured region from mask + depth, a map cross-check and a re-prompt tracking gate. Pure numpy, no ROS; the producer node that wires it is a follow-up. Every threshold is a calibration point; the caps are the Safety-WG placeholders.
class TargetRefusal(StrEnum)(L114) —TOO_FEW_CELLS,AMBIGUOUS(another cluster ≥ambiguity_ratioof the anchored target's — HZ-0115-2),TOO_FEW_POINTS,NO_HEIGHT_ABOVE_SUPPORT,NOT_ON_SUPPORT(the mask cloud ends > 2 voxels above the support — HZ-0115-6),HALF_EXTENT_CAP,VOLUME_CAP,STACKED(a candidate mask holding a nested candidate refusedNOT_ON_SUPPORTwith ≥ 2 voxels of height — HZ-0115-6)._REFUSAL_SEVERITYorders them most severe first.class VoxelLattice(frame_id, origin, orientation_xyzw, resolution, size, occupancy)(L158) — TheOccupancyVoxelsfields, same convention asbucket2_markers.occupied_voxel_centers(re-derived, cross-checked intests/unit/test_grasp_target.py). RaisesROSConfigErroron a length mismatch, non-positive resolution or non-unit orientation.occupied_centers()is computed once per lattice and cached read-only (the producer legs warm it at grid receipt, outside the vision bridge's lock).rotation() -> NDArray(L202) — lattice axes →frame_id.occupied_centers() -> NDArray(L206) —(N, 3)occupied cell centres inframe_id, vectorised.class TargetSeed(L225) —point | None,refusal | None,cluster_sizes(the anchored target's first, the rest descending),bottom_z | None(the selected cluster's lowest cell's bottom face).- module const
EXTRINSIC_ERROR_M = 0.01(L141) — Camera-mount error budget of a target fit (target_region_from_mask's default). Calibration point. fit_pad_m(resolution, extrinsic_error_m=EXTRINSIC_ERROR_M) -> float(L144) — The padding a target fit adds to its sides and top: half a cell diagonal plus the mount error.class TargetRegionFit(L243) —region: PlaceRegion | None,refusal | None,point_count,depth_valid_fraction,half_extents,box: PlaceRegion | None = None(the fitted box on aNOT_ON_SUPPORTrefusal — the visible part — for the leg's occlusion test,_refused_fit),z_span: float = 0.0(trimmed cloud height, top minus lowest point;target_region_from_masksvetoes on it),support_z: float | None = None(the support the region stands on: measured, or one voxel under the target's own lowest kept point when unseen — the lower face is alwayssupport_z + resolution),support_unseen: bool = False.occupied_centers_in_box(grid, box: PlaceRegion) -> NDArray(L291) — Occupied centres inside an oriented box (the declaration's search box — reasoner-grounded, or a direct-dispatch scene's); refuses a frame mismatch withROSConfigError.support_top_from_voxels(grid, centers, *, near_xy, min_cells, probe_margin_m=0.05, surface_centers=None) -> float | None(L333) — The measured support the target stands on (HZ-0115-6): scanning the column's z-layers top-down, the first layer holding ≥min_cellstop-surface cells (nothing occupied directly above) in the ring between one cell andprobe_margin_maround the footprint of the component above it holding the anchor (the highest cell of the occupied column nearestnear_xy, the same cell at every layer, so a taller neighbour never turns the target's top into its support, HZ-0115-2) — a surface extends past what stands on it, the target's own layers (a dense top included) do not; the probe scales with the target. The ring is counted onsurface_centers(defaultcenters) — it lies outside the target's footprint, so mostly outside a tightly padded search column; the leg passes the column grown byprobe_margin_m+ one cell.Nonewhen no layer qualifies. Ceiling: a target flaring > 1 cell can stop the scan inside it (region stands higher — less exemption).ROSConfigErroron a tilted (non-z-up) lattice or aprobe_margin_munder two cells (an empty ring; never silently widened — the leg checks first and reports aprobe_margin_under_two_cellslost view).target_seed_from_voxels(grid, centers, *, near_xy, support_z, min_cells, ambiguity_ratio=0.5) -> TargetSeed(L505) — Drops cells within one voxel of the support plane, labels 26-connected components on the lattice, returns the top-centre of the cluster holding the anchor (the highest cell of the occupied column nearestnear_xy— never merely the largest); refuses too few cells or any other cluster ≥ambiguity_ratioof the target's size (HZ-0115-2: a taller neighbour is ambiguous, not the target).project_point(point_base, t_cam_from_base, intrinsics) -> tuple[float, float] | None(L569) — Base-frame point → REP-103 optical pixel(u, v)(pixel-centre convention ofbackproject_masked_depth);Nonebehind the camera or outside the image. SAM 2.1's positive point prompt.target_region_from_mask(mask, depth_m, intrinsics, t_base_from_cam, *, support_z: float | None, resolution, frame_id, evidence_ref, stamp_ns=0, erode_px=2, extrinsic_error_m=0.01, trim_percentile=1.0, min_points=200, min_depth_m=0.1, max_depth_m=3.0, max_half_extent_m=0.20, max_volume_m3=0.03) -> TargetRegionFit(L680) — Erode →backproject_masked_depth→ base frame → percentile-trimmed, gravity-aligned (yaw-only, 2-D PCA footprint) box; lower face fixed atsupport_z + resolution(never below — HZ-0115-6) — with the support unseen (support_z=None, or a measured layer more than a voxel above the target's own lowest kept point: a neighbour's top, a bin rim) at the target's own lowest kept point instead, never lower, and noNOT_ON_SUPPORTcheck; sides and top padded by√3·resolution/2 + extrinsic_error_m; refuses on the caps, andNOT_ON_SUPPORTwhen the trimmed cloud's lowest point sits more than two voxels abovesupport_z(a target on a same-footprint box or a hidden riser clusters with it in the voxel seed, but the mask names the target alone — and a view of its top face only is refused too).target_region_from_masks(masks, depth_m, intrinsics, t_base_from_cam, *, resolution, nested_fraction=0.9, **fit_kwargs) -> TargetRegionFit(L843) — The segmenter's candidates (subpart/part/whole) throughtarget_region_from_mask: the first that fits in the segmenter's order, except one holding ≥nested_fractionof a smaller candidate refusedNOT_ON_SUPPORTwhose cloud spans ≥ 2 voxels (the body it stands on would be exempted) is refusedSTACKED; a top face alone (< 2 voxels, a can's lid) vetoes nothing. With no candidate fitting, the first such body holding no other one is re-fitted with the support unseen: its region stops at its own lowest point, so what it stands on (a bin floor, a riser) stays outside. A set refused whole reports its most severe refusal (_REFUSAL_SEVERITY), not the last tried. Ceiling: a tall subpart vetoes its whole object (less exemption). Empty →TOO_FEW_POINTS.mask_without_removed_points(mask, depth_m, intrinsics, t_base_from_cam, kept_points_base, *, cell_m=0.005) -> NDArray[np.bool_](L621) —maskwithout the pixels whose depth point the robot self-filter removed from the same capture (kept_points_base:robot_self_filter's output in the base frame): a pixel survives only with a kept point in its own or a neighbouringcell_mcell. Reuses the self-filter's own verdict (collision model at the measured joint state) instead of re-modelling the robot; only ever shrinks the fit. A pixel's verdict does not depend on the mask, so the leg filters the union of the segmenter's candidates once per capture and intersects each candidate with it.region_covers_occupied(grid, region, *, min_fraction=0.5) -> tuple[int, bool](L942) — Occupied cells with centres in the region vs.min_fractionof the region's footprint cell count; a region the map does not see is refused.track_region(previous, current, *, max_centroid_shift_m, extents_tol_m) -> bool(L989) — The 2-5 Hz re-prompt gate: same frame, centroid shift and (sorted-horizontal) half-extent change within tolerance.cell_closed_region(region, grid, *, max_half_extent_m) -> tuple[PlaceRegion, bool](L1070) —(closed, clamped): a yaw-only fit grown so every lattice cell it intersects has its centre inside (the kernel's cell-centre exemption test):r/2·Σ|(Rᵀ_box R_grid)_ij|per local axis —r/2·(|cos θ|+|sin θ|) ≤ r/√2horizontally against a z-up lattice — andr/2up only, the bottom kept (the support layer stays non-exempt). Clamped per half-extent atmax_half_extent_m. Applied only to the region the grasp-target leg publishes;ROSConfigErroron another frame or a tilted region.lowered_to_support(region, support_z) -> PlaceRegion(L1134) —regionwith its lower face moved down ontosupport_z, the rest unchanged (regionitself when the face is not above it). The vision-attached region payload is lowered from one voxel above the measured support onto its top, so the target's own bottom layer is payload, not an obstacle under it once lifted (Isaac i56/i57); the support's cells stay outside.margin_grown_region(region, margin_m, *, down) -> PlaceRegion(L1180) — The grasp-target bloat (HZ-0115-32):regiongrown bymargin_malong its own axes on the four sides and the top, and the bottom too whendown(the kernel's pre-handover region;down=Falseis the attached payload, never into the support).margin_m = 0returnsregionitself;ROSConfigErroron a negative or non-finite margin.region_within(inner, outer, *, tol_m) -> bool(L1033) — Every corner ofinnerinsideoutergrown bytol_m(false across frames): tells a re-fit shrunk by the approaching hand's occlusion from one that reaches outside the held region.occupied_touching_outside(grid, region, *, support_z) -> NDArray(L1234) — Occupied cell centres more than one voxel abovesupport_z, outsideregionand 26-adjacent to an occupied cell inside it. Empty iff the region holds the whole 26-connected map component of the cells it contains (any path out crosses that ring), so it looks one cell around the region and needs no bound on the component. The grasp-target leg runs it on the kernel's (cell-closed) region before accepting a fit; a non-empty result sends the fit tomap_completed_region(partial_fitwhen that refuses).map_completed_region(grid, fit, column, *, support_z, max_half_extent_m, max_volume_m3) -> tuple[PlaceRegion | None, int, str](L1302) —(completed, added, reason): the camera's fit grown to the target's whole map component (Isaac i45: the hovering hand hides the far side; the map remembers it from earlier views). The component: occupied cells more than one voxel abovesupport_z, 26-connected, seeded from those incell_closed_region(fit), flooded insidecolumn(the leg's search column). The result is the smallest box in the fit's yaw holding the fit and every component cell's centre, its bottom the fit's own (never down); its cell closure (what the kernel gets) then holds every component cell whole.Nonewith the reason when a component cell's 26-neighbour lies outsidecolumn(touches the search edge), the box exceeds the caps, or its closure holds an occupied above-support cell not of the component (another body in its corners — checked over the closure's own extent, so a box in a yaw other than the column's has its corners tested too).added= component cells beyond the fit's closure. A body within one voxel of the target merges into it (HZ-0115-30).ROSConfigErroron a frame mismatch or a tilted fit.
python/hal/src/openral_hal/_object_primitives.py
Object instances before contact (docs/reference/object-primitives-design.md, issue #349): camera-originated, map-confirmed, tracked. The grasp-target leg (premeasure=True) prompts SegmentInView where instance_prompt says, fits each mask with the camera path's own fit, and keeps the box only when map_confirms it; PrimitiveTracker holds identities, nearest_primitive picks the instance a hand approaches. The map never originates a box. Pure numpy, no ROS, no clock; reuses _grasp_target's lattice, components, cover and tracking gates.
class PrimitiveFit(region, cell_count, support_z)(L59) — One camera instance fit, map-confirmed: the box (target_region_from_masks), the occupied map cells inside it, the support top it was fitted on.class ObjectPrimitive(primitive_id, region, cell_count, support_z, first_seen_ns, last_seen_ns)(L76) — A tracked instance;last_seen_nsis its newest camera fit's stamp (its confirmation), what staleness counts from.map_confirms(grid, region, *, support_z, min_cover, pad_m=0.0, own=()) -> tuple[str, str](L97) —("", "")when the map confirms a camera box, else(kind, why):map_disagrees(region_covers_occupiedbelowmin_cover: the object gone, or never in the map) ormap_split(the occupied above-support cells in the box's cell closure are more than one 26-connected body). A touching neighbour is one body with it, so a packed pair passes, each instance with its neighbour's sliver, never the merged blob.ROSConfigErroron a frame mismatch. A second body confined to the bandpad_minside the box's sides and top (the fit's padding,fit_pad_m) is a neighbour's sliver, not a split; one wholly inside theownboxes (the robot's hands) is the robot's own, not a split.instance_prompt(grid, column, covered, *, near_xy, skip=()) -> tuple[point, cell] | None(L188) — Where to prompt the segmenter next (SegmentInViewhas no prompt-free mode): the raised top-surface cell (no occupied cell above it, more than one cell above the column's lowest top surface) nearestnear_xy, in nocoveredbox and not inskip, preferring interior cells (all four side neighbours raised tops) so a packed pair's shared top is prompted one cell inside one body, not on the seam. Returns the cell's top-face centre and the cell (theskipkey).point_box_gap_m(point, region) -> float(L263) — Distance from a point to an oriented box (0 inside).nearest_primitive(regions, hand_points, *, ambiguity_m) -> tuple[int | None, str](L282) — The instance with the smallest gap to any hand point;ambiguouswhen the runner-up is withinambiguity_m(one voxel) of it (HZ-0115-2), or when a named search box (no hand points) holds more than one.(index, "")or(None, reason).class PrimitiveTracker(*, max_misses=3, max_tracks=16)(L336) — Identities across fits, label-free: a fit withintrack_region's one-voxel tolerance refreshes its instance; one whose centre lies in an instance's box (or holds its centre) is the same object nudged (identity kept, box replaced); anything else is new. Pastmax_tracksthe instance seen longest ago is dropped. Single-threaded by contract (the leg, under the bridge lock).update(fits, *, stamp_ns, tol_m, scanned) -> list[ObjectPrimitive](L417) — Fold fits in; an instance whose centre lies inscannedthat nothing matched accrues a miss and is dropped aftermax_misses; one outside is kept. Returns the fits' instances, largest first.prune(keep) -> list[ObjectPrimitive](L459) — Drop every instancekeeprefuses (the leg's staleness and map-contradiction rules); returns the dropped.- (property)
primitives -> list[ObjectPrimitive](L398) — every tracked instance.
python/hal/src/openral_hal/_grasp_trigger.py
When to ask perception "what is in the jaws?" — a debounced state machine over the gripper joint's commanded target and measured position (no effort: the real OpenArm driver hard-codes it to 0.0). Pure: no ROS, no numpy, no clock. _vision_attachment_evidence.py is told a grasp happened; this is what tells it. Thresholds are the joint's manifest closure_calibration; an uncalibrated gripper refuses to arm.
class GraspEvent(str, Enum)(L93) —ATTACH(empty → loaded: segment, gate, attach),REGRASP(still loaded but the jaw re-settled materially elsewhere, so the fitted geometry is stale: segment again),DETACH(loaded → empty: publish an empty attachment set; no segmentation).- module const
GAP_PERIODS = 4.0(L90) — sample periodsfor_ratetolerates between accepted samples before a window restarts. class PositionStallConfig(L114) — Frozen dataclass, robot-independent windows only, in seconds of sample time (JointState.stamp_ns) so they mean the same at any joint-state rate:consecutive_s=0.06,settle_s=0.13(the spans 3 and 5 samples covered at 30 Hz), plus minimum sample countsconsecutive_samples=3,settle_samples=5(every window must hold on both time and count, so late ticks cannot confirm early),max_gap_s=0.1(a longer gap between accepted samples restarts every window),min_sample_interval_s=5e-4. Calibration points.for_rate(rate_hz, *, max_gap_s=None) -> PositionStallConfig[@classmethod] (L176) — The windows for a stream fed atrate_hz: onlymax_gap_sis rate-bound,max(0.1, GAP_PERIODS / rate_hz)(a fixed 0.1 s is one period at 10 Hz, so neither the trigger nor the heartbeat evidence ever confirmed);max_gap_soverrides it. The time windows stay (their sample-count floors already stretch them at slow rates).ROSConfigErroron a non-positive / non-finite rate or a degenerate override.validate() -> None(L152) —ROSConfigErroron a non-finite or negative window,settle_s == 0,max_gap_s <= min_sample_interval_s,consecutive_samples < 1orsettle_samples < 2.is_gap(stamp_ns, last_ns) -> bool(L209) — a sample stamped more thanmax_gap_safter the last accepted one; restarts the trigger's windows and the bridge's_JawEvidencerun.is_repeat(stamp_ns, last_ns) -> bool(L220) — a sample stamped no later thanlast_ns + min_sample_interval_srepeats (or predates) the last accepted one; shared by the trigger and the bridge's_JawEvidence.gripper_joints(description) -> list[JointSpec](L232) — Everyrole: "gripper"joint, in manifest order (one per hand on a bimanual robot). RaisesROSConfigErroronly on zero.gripper_joint(description, joint_name=None) -> JointSpec(L258) — One gripper joint: the named one (ROSConfigErrorif it is not gripper-role), or withNonethe manifest's single one — zero or several raise.class PositionStallTrigger(description, *, joint_name=None, config=None)(L299) — ATTACH when the command is withinstall_gapofclosed_position, the jaw is settled (span ≤settle_toleranceoversettle_s) and sits further from closed than the command by more thanclosed_rest_offset + stall_gap; DETACH when, still commanded closed, that gap collapses to ≤rest + stall_gap/2(slip / closed on nothing), or when commanded open and the jaw opens more thanstall_gap/2past its hold; REGRASP when still stalled butstall_gap/2from the hold. "Settled" is the span over at leastsettle_samplessamples covering the lastsettle_s; every transition needs at leastconsecutive_samplesagreeing samples spanningconsecutive_s; a gap overmax_gap_srestarts both. A repeated stamp (is_repeat) is dropped and counted inrepeated_samples. RaisesROSConfigErroron an unresolvable joint, noclosure_calibration, no position sensor, aclosed_positionthat is not an end ofposition_limits, or a degenerate window.command(target) -> None(L420) — Fold in the jaw's commanded target (joint units); non-finite ignored.clear_command() -> None(L429) — Forget the command (it is unknown): no ATTACH / REGRASP / DETACH until the nextcommand; a held payload stays held. Measuring against a stale close is how an open, stationary jaw reads as a grasp.update(state) -> GraspEvent | None(L438) — Fold one snapshot in. A tick with no finite position for the joint incrementsmissing_position_ticksand clears the settle window; a tick before any command incrementsuncommanded_ticksand can never attach; a repeated stamp incrementsrepeated_samples, a gap pastmax_gap_sincrementsgap_resetsand restarts every window.- (property)
joint_name -> str(L396) — the resolved gripper joint's manifest name. - (property)
attached -> bool(L401) — current latched grasp state. - (property)
last_command -> float | None(L406) — the last commanded target,Nonebefore the first. - (property)
thresholds -> tuple[float, float, float, float](L416) —(closed_position, closed_rest_offset, stall_gap, settle_tolerance)in force. - (property)
closing -> bool(L411) — the last command is withinstall_gapof closed, the only band ATTACH reads. - attributes
missing_position_ticks,uncommanded_ticks,repeated_samples,gap_resets(a gap pastmax_gap_srestarted every window) — health counters;last_short_of_command,last_settle_spread— the last classified sample's margin to the command and the jaw's position span over the settle window (Nonebefore one). Read byVisionAttachmentBridge._log_close; no decision reads them.
python/hal/src/openral_hal/vision_attachment_bridge.py
The ROS wiring that turns _grasp_trigger events into openral_msgs/srv/SegmentInView calls, feeds the replies to VisionAttachmentEvidenceProducer, and publishes the resulting AttachmentState the safety kernel already consumes. The real-hardware, opt-in sibling of SimSensorBridge's attachment leg (the two must not both drive /openral/attachment_state) — torch-free by construction, since everything model-shaped lives behind the service in openral_perception_ros.segmenter_node.
The deferred-ack barrier. The HAL holds a grouped tick's action_applied until attached-payload perception settles; segmentation runs inside that same wait. The wait is bounded (deadline_s), never skipped (every failure path ends in the conservative jaw-span box at GRIPPER_CLOSURE), and visible (every fallback logs its typed reason).
- module const
DEFAULT_SEGMENT_SERVICE(L199) ("/openral/perception/segment_in_view") — shared withsegmenter_node.DEFAULT_SEGMENT_SERVICE; a unit test asserts the two halves agree. - module const
MAX_RELEASE_CLEAR_M(L207) (0.1) — Ceiling onVisionAttachmentConfig.release_clear_m(refused above,ROSConfigErrorat bridge construction, i.e. at HAL activate): the deploy's derivation is the kernel's world margin + one octree cell (0.04 m on the real cell); 0.1 m leaves headroom for a 5 cm margin over a coarse 5 cm cell. It also bounds the region payload's support-witness rise tolerance, so a params-file 0.3 cannot keep the support exemption through a 30 cm carry. class VisionAttachmentConfig(L1078) — Frozen dataclass:self_filtered_cloud_topic=""(the robot self-filter's output for this camera, set by the deploy launch when the filter runs — the grasp target fit drops the robot's own points withmask_without_removed_points),camera=""(empty picks the manifest's first camera with intrinsics; only the id and its existence come from the manifest),depth_topic=""(empty →camera_topic(camera, CameraTopicKind.DEPTH_IMAGE); itsheader.frame_idis the optical frame tf2 is asked for),camera_info_topic=None(empty/None →camera_topic(camera, CameraTopicKind.DEPTH_CAMERA_INFO); HAL paramvision_attachment_camera_info_topic; the only K ever projected through — no message, an uncalibrated one, or a size that differs from the depth raster is a typedGRIPPER_CLOSUREfallback, never the manifest's nominal K),service_name=DEFAULT_SEGMENT_SERVICE,deadline_s=0.25,tcp_frame=""(empty → the gripper joint'schild_link, an approximation — no schema field carries a calibrated TCP today),jaw_tip_frames=(),object_id="grasped_payload"(suffixed:<gripper joint>per leg),evidence_timeout_s=0.5(calibration point; how old the newest sample with a finite jaw position for every gripper may be before the attachment heartbeat stops; HAL paramvision_attachment_evidence_timeout_s),evidence_run_spans_gaps=False(sim twin only, set by the HAL node fromtwin_jaw_evidence_timeout_s: a sample gap shorter thanevidence_timeout_sdoes not restart the_JawEvidencerun),tf_frames={}(manifest link → tf2 frame, used only for tf2 lookups; each entry must be the same rigid body, e.g. OpenArmopenarm_left_link7→openarm_left_ee_base_link; keys must be manifest links), and the pre-grasp target leg'sgrasp_target_enabled=False,grasp_target_rate_hz=3.0(2-5 Hz, elseROSConfigError),grasp_target_freeze_s=None(unset =2 × grid_max_age_s; HAL paramvision_attachment_grasp_target_freeze_s, 0 = unset),grasp_target_min_cells=8,grasp_target_min_cover=0.5,grasp_target_support_search_below_m=0.15(≤ 0.5 m),grasp_target_support_probe_margin_m=0.05(≥ two grid cells, else a lost view),grasp_target_occluder_margin_m=0.05(≤ 0.10 m),grasp_target_approach_m=None(the approach-armed target, off; HAL paramvision_attachment_grasp_target_approach_m, 0 = off),grasp_target_margin_m=VisionAttachmentRuntime.DEFAULT_GRASP_TARGET_MARGIN_M(0.0 — no bloat unless the scene names one; the grasp-target bloat,[0, 0.05]m elseROSConfigErrorfromGraspTargetLeg; HAL paramvision_attachment_grasp_target_margin_m, 0 = unbloated; HZ-0115-32),grasp_target_premeasure=False(pre-measure object instances from head-camera segmentation before any hand arms, map-confirmed —_object_primitives,GraspTargetLeg(premeasure=True); HAL paramvision_attachment_grasp_target_premeasure; prototype, issue #349) (HAL paramsvision_attachment_grasp_target_*; the bridge hands the last three toGraspTargetLeg; calibration points), and the release window'srelease_clear_m=0.04(the deploy passes the kernel's world-voxel margin + one voxel resolution; 0.04 is the real cell's 20 mm + 20 mm; the default serves direct construction (tests) only —ManifestHALLifecycleNoderefuses the leg with it unset; HAL paramvision_attachment_release_clear_m; also the region payload's support-witness rise tolerance; at mostMAX_RELEASE_CLEAR_M= 0.1 m, elseROSConfigError) andrelease_timeout_s=3.0(HAL paramvision_attachment_release_timeout_s; calibration points; both must be positive, elseROSConfigError),grid_max_age_s=1.0(oldest world-voxel grid the grasp-target and place-fixture legs use; the deploy passes the kernel'sworld_voxel_deadline_s, 1.0 s is the real cell's; the default serves direct construction (tests) only —ManifestHALLifecycleNoderefuses the leg with it unset; HAL paramvision_attachment_grid_max_age_s; must be positive), andmask_depth_max_skew_s=0.1(HAL paramvision_attachment_mask_depth_max_skew_s; largest|mask capture stamp - depth stamp|aSegmentInViewmask is back-projected across — a same-grab ZED pair is 0, 0.1 s admits one frame of cache lag at ≥ 10 Hz; beyond it the attachment path falls back toGRIPPER_CLOSUREand the grasp-target leg takes amask_depth_skewlost view; must be positive; calibration point).tcp_frame/jaw_tip_framesare single-gripper only and raise on a multi-gripper manifest; there the TCP is each gripper joint'sorigin_xyz, read without TF.deadline_sis sized off a warmed GPU segmenter call plus round trip; a CPU-only host must raise it deliberately or every grasp falls back. Place leg:place_target_enabled=False,place_target_rate_hz=2.0,place_target_freeze_s=None(= and at most2 * grid_max_age_s),place_target_search_depth_m=0.20,place_target_extrinsic_error_m=MAX_PLANAR_ERR_M(HAL paramsvision_attachment_place_target_*; calibration points).class SegmentOutcome(L1295) — Frozen(use_masks: bool, reason: str).resolve_segment_outcome(*, timed_out, ok, failure_reason, mask_count) -> SegmentOutcome(L1309) — Pure decision for one round trip: a deadline miss wins asROSDeadlineMissed:;ok=Falseuses the server's typedfailure_reason(or aROSPerceptionStale:default);ok=Truewith zero masks is alsoROSPerceptionStale:. No input combination yields an unexplained fallback.build_segment_request(*, stamp_ns, camera, t_link_from_cam, tcp_in_link, negatives_in_link=()) -> SegmentInView.Request(L1428) — The request the bridge sends:frame_id=""(the segmenter's "already in my optical frame") with the TCP and jaw-tip prompts moved into the optical frame byinv(t_link_from_cam), so the segmenter never re-resolves a link its TF tree may lack. Precondition: the segmenter's RGB is registered to the depth stream (true for the ZEDrgb/color/rect/depth/depth_registeredpair).decode_mono8_mask(data, *, height, width) -> NDArray[np.bool_](L1358) — Reader half ofsegmenter_node.mono8_bytes_from_mask; any non-zero pixel is set. RaisesROSConfigErroron a length mismatch.mask_depth_skew_reason(mask_stamps_ns, depth_stamp_ns, *, max_skew_s) -> str(L1388) —ROSPerceptionStale: …when any mask's capture stamp is more thanmax_skew_sfrom the depth frame it would be back-projected through, else"". Applied by the bridge's_on_reply(→GRIPPER_CLOSUREfallback, reason logged) and the grasp-target leg (→mask_depth_skewlost view).mask_stamps_ns(masks) -> list[int](L1423) — Eachsensor_msgs/Imagemask'sheader.stampin ns (the segmenter copies the RGB frame's capture stamp).class VisionAttachmentBridge(node, description, *, on_perception_ready=None, config=None, gate_config=None, trigger_config=None)(L1474) —trigger_configis onePositionStallConfig(debounce) for every hand and the heartbeat evidence;Nonederives it from the manifest'scontrol_rate_hz(PositionStallConfig.for_rate, the 30 Hz-tuned defaults when undeclared) — the HAL node passes one derived from its own joint-state rate; the thresholds are each gripper joint'sclosure_calibration(an uncalibrated gripper is aROSConfigError). One leg (private_GripperLeg:PositionStallTrigger, producer, TCP, current attachment, in-flight request, deadline timer,generation,region_spent,support_anchor—(frame, ATTACH origin, rise tol, slide tol),regrasp_hold: AttachedCollisionObject | None— the region payload a REGRASP began on, kept when its segmentation is rejected, HZ-0115-34) perrole: gripperjoint; every publish carries the union of the legs' attachments.__init__creates no ROS entities. Owns the depth and depth-CameraInfosubscriptions (sensor-data QoS), tf2 listener,SegmentInViewclient and the/openral/attachment_statepublisher (RELIABLE/TRANSIENT_LOCAL, matching the sim bridge). RaisesROSConfigErrorat construction when the manifest can't support the producer or the trigger.setup()seeds the revision from the node clock (host wall time on real) so a re-activate never moves it backwards (+1 per event; nothing persisted), and starts a 0.2 s heartbeat that republishes the union at the current revision with a fresh stamp — only while jaw-position evidence is live (private_JawEvidence: every gripper reported a finite position withinevidence_timeout_s, in an unbroken run of new — not repeated-stamp — samples spanningPositionStallConfig.consecutive_sand at leastconsecutive_samplesof them, re-earned after a monotonic timeout or a sample-time gap overmax_gap_s— overevidence_timeout_sinstead withevidence_run_spans_gaps, the twin), no grasp is being resolved, and every trigger agrees with its leg's attachment; otherwise it is withheld (logged once per transition) so the kernel's snapshot ages into a drop rather than a stale "nothing attached". Back-projection runs in the cached depth frame'sheader.frame_id(empty → typedROSPerceptionStalefallback; logged once at info when it differs from the manifest'sframe_id), through the liveCameraInfointrinsics, with masks resampled nearest-neighbour onto the depth raster only when the aspect agrees within 1e-3 (else a typedROSConfigErrorfallback). Withgrasp_target_enabledit owns a_grasp_target_leg.GraspTargetLegand every publication (event and heartbeat) carries itsgrasp_declaration_valid/grasp_declaration; an ATTACH on a leg whose jaw link (_GripperLeg.jaw_link, the gripper joint'schild_link) the declaration names stops re-measurement, and — when the envelope (TTL/expiry applied) still holds a region and the jaw is at it (jaw_at) — attaches that region — closed over the map cells it touches, the box the kernel latched (GraspTargetLeg.kernel_region, Isaac i50; tight vs closed half-extents logged once) — as the payload (region_attachment, private_region_payload) without aSegmentInViewcall (design §2.2 "Handover") — once per leg per measured region (region_spent=(target_id, region.stamp_ns)), handed toGraspTargetLeg.on_attachwith the region it was built from; a segmented ATTACH (_finish) goes throughGraspTargetLeg.on_attach(region=None), which hands the region over only for a payload on it; a REGRASP, or a later ATTACH offered the same region, segments; otherwise it segments, logging why the region was not used. Withplace_target_enabledit owns a_place_target_leg.PlaceTargetLeg: every publication carries itsplace_declaration_valid/place_declaration(dispatch's goal-scope one — the leg never declares on its own — with the slab measured under the carried payload as region) and the attachment set passes throughPlaceTargetLeg.decorate(the map-support proximity witness);observe_joint_statefeedsPlaceTargetLeg.on_joint_state. A DETACH opens that leg'sReleaseWindow(the payload stays published, frozen on the base link) instead of dropping it; the heartbeat polls every window first and publishes at a fresh revision when one closes. Every publish at a fresh revision whose set lost or replaced an object, or is empty, first callsGraspTargetLeg.on_attachment_changed(emptiedpassed: a mid-grasp hand's identity — never its region — is kept across a change that leaves the set non-empty), so that very snapshot's envelope already names the other hand's fresh identity with no pre-change region (the kernel retires an armed declaration at every detach edge, HZ-0115-3). Every DETACH (also one that finds nothing latched) and every barrier release re-checks the hand's pick (GraspTargetLeg.on_hand_settled). Threading: one re-entrant bridge lock (_lock, replacing the former publish and release locks) serializes every entry point that reads or mutates leg, tracker or attachment state —observe_joint_state/observe_command/clear_command/attachment_action_ack_ready/missing_position_ticks, the heartbeat and release poll, every publish, the segmentation reply/deadline commits,teardown, and the producer legs' ticks, replies, deadlines and declaration callbacks (the legs alias it). Order: bridge lock →GraspTargetTracker's lock, never the reverse. Slow pure work runs outside it and commits only under an unchangedgeneration: a reply's mask decode + back-projection (_finish,GraspTargetLeg._measure) and the grasp leg's column scan (GraspTargetLeg._seed); tf2 lookups (latest, no timeout) run under it. A malformedSegmentInViewreply (mask bytes vs shape, a bad field) resolves like the deadline — theGRIPPER_CLOSUREbox, the barrier released — never an exception out of the executor. The bridge owns the ONE/openral/world_voxelssubscription (_on_voxels, the kernel's RELIABLE/VOLATILE/KEEP_LAST 1, created when either producer leg is enabled): each grid is decoded once into_grid(lattice,source_stampns, receive time), shared by both legs, its whole-grid occupied-cell scan deferred to first use and cached on theVoxelLattice. Real hardware runs every caller on the executor's one thread, so behaviour there is unchanged. Covered bytests/unit/test_grasp_target_leg.py::test_every_bridge_and_leg_entry_point_is_serializedand::test_the_lock_order_is_bridge_then_tracker_never_the_reverse. Construction refuses abase_framethat is not the kinematic tree's root (_release_base_link). Covered live bytests/integration/test_vision_attachment_heartbeat_live.py,tests/integration/test_vision_attachment_optical_frame_live.py,tests/integration/test_grasp_target_leg_live.pyandtests/integration/test_vision_attachment_release_window_live.py.setup() -> None(L1622) — Create the ROS entities.teardown() -> None(L1705) — Destroy the ROS entities. Idempotent and always reopens the barrier, so a mid-flight deactivate can never leave the node deferring an acknowledgement forever.tf_frame(link) -> str(L1756) — The tf2 frame a manifest link is looked up as (tf_frames, else the link itself).jaw_point(jaw_link, frame) -> tuple[float, float, float] | None(L2665) — Where the hand whose jaw link isjaw_linkis, in tf2frame: the leg's attach link through tf2 (tf_frames-mapped) composed with its TCP — the attachment prompt's own point, never a tf lookup of the jaw link (the OpenArm'sopenarm_<side>_finger_pairis a manifest-only link).Nonefor a link no leg owns or a missing transform. Used by the grasp-target leg's occlusion gate.jaw_at(jaw_link, region, *, reach_m) -> tuple[bool, str](L2692) — Whether the hand whose jaw link isjaw_linkis at the heldregion, with the measure as log text: its TCP or its closing midpoint withinregiongrown byreach_m(separating-axis gap of the point on the region's axes). The closing midpoint is the mean centre ofjaw_link's manifest collision primitives (bounded boxes, private_link_boxes— shared withReleaseWindow.open) posed by the attach link's tf2 pose · gripper joint origin ·_joint_motion(q)at the last read jaw angle — between the fingers; never a box overlap, which on the OpenArm's opening-spanning finger_pair box (half 0.029 × 0.071 × 0.081 m) accepted a neighbour ~7 cm beside the jaw (PR #346 review). Unknown angle / no geometry / no tf2 → the TCP alone / not at it, said in the measure. The ATTACH test of_region_payloadandGraspTargetLeg.on_attach(segmented payload); the TCP may be the finger hinge (OpenArm), a finger length above what the jaws close on (Isaac i41: midpoint 13.5 mm off the can, hinge 9 cm)._on_kept_cloud(msg) -> None(L2788) — Caches the self-filter'sPointCloud2(skip_nans, x/y/z) by capture stamp and frame, the last 8; the subscription exists only withself_filtered_cloud_topicset and is destroyed (cache cleared) byteardown.kept_points(stamp_ns, frame) -> NDArray[np.float64] | None(L2796) — The robot self-filter's kept points for the capture neareststamp_ns(withinmask_depth_max_skew_s; clouds are best-effort and some are lost, and a neighbouring capture can only remove more mask pixels; last 8 clouds onself_filtered_cloud_topic), inframe;Nonewithout that capture or its tf2 — the grasp target leg then refuses the capture as the lost viewunfiltered.- (property)
gripper_joint_names -> tuple[str, ...](L1768) — one leg per name, manifest order. attachment_action_ack_ready() -> bool(L1748) — True only when no leg is pending. The same shapeSimSensorBridgeexposes, soManifestHALLifecycleNode's deferred-ack path treats both identically.observe_joint_state(state) -> None(L1775) — Called from_publish_joint_statewith the same typed snapshot the HAL just read (no second source of truth). Fed to every leg (and toPlaceTargetLeg.on_joint_statewhen that leg is on); also caches every joint position by name (the jaw angles for the release test).DETACHmoves that leg's attachment into aReleaseWindow(no tf2 hand pose → dropped at once, logged) and republishes;ATTACH/REGRASPcloses an open window (reason=attach), closes the barrier for that leg and dispatches one bounded request. Every event first supersedes the leg's in-flight request (private_supersede: generation +1, deadline timer cancelled and destroyed, future dropped then cancelled); replies and deadlines carry their generation and a stale one is dropped, so a DETACH in flight resolves to no attachment and releases the barrier._log_close(leg, event) -> None(L1938) — Per leg, fromobserve_joint_state: one line when a close starts (command withinstall_gapof closed, jaws empty) and one at its outcome — the ATTACH, or the command leaving that band without one — with that close'sgap_resets/repeated_samplesand the trigger's lastshort_of_command/ settle spread. Logging only.observe_command(action) -> None(L1972) — Called by_on_safe_actionwith the command the HAL applied (called byHALLifecycleNodeBase._fold_applied_commandfrom_on_safe_action: an ungrouped action as sent; for an ADR-0102 slot group the HAL'slast_applied_action— the composed full-dof action — once itslast_committed_tick/_sessionmatch the slot's, a slot that only staged changes nothing; a committed group whose command is unknown —last_applied_actionisNone, or the HAL has none, logged once — callsclear_commandinstead, never keeping the old command); the last row (the trajectory goal) is the command. The bridge stages no slots: a slot (tick_group_size > 1) handed to it is ignored and logged once. An ungroupedJOINT_POSITIONrow: full dof → padded, read at each owned joint's manifest index; as long as itsjoint_names→ compact, positional; any other length → logged once, ignored. Other modes leave every leg's last command untouched.clear_command() -> None(L2012) —PositionStallTrigger.clear_commandon every leg: the HAL applied a command nobody can read.- (property)
missing_position_ticks -> int(L2051) — The legs' missing/non-finite jaw-position counters, summed. _retire_lifted_supports() -> None[private] (L2556) — Fromobserve_joint_state, throttled to the place leg's 20 Hz witness period: drops a region payload's witness for good (same object and stamp, so the kernel's (object, support, stamp) key never re-arms) once the payload frame, in the support plane's frame (z up), rose more thanmax(resolution, extrinsic_error_m, release_clear_m)(the kernel's world margin plus a cell: the payload's lower face rests on the support top;release_clear_mcapped atMAX_RELEASE_CLEAR_M) or moved horizontally more thanmax(resolution, extrinsic_error_m)(the margin buys nothing sideways) from its ATTACH pose (_GripperLeg.support_anchor), or tf2 cannot place it; sinking keeps it (same plane, the kernel bounds the penetration) — the kernel's own liveness reads the map, and the octomap bridge withholds the witness band, which rides with the payload over the carried object's own lowest cells. Republishes at the current revision while the heartbeat is open.clip_payload_behind_tool_point(obj, tcp_in_link) -> AttachedCollisionObject(L658) — Every box primitive ofobjclipped so nothing lies behind the tool point along the approach (an axis within 30°, front face kept); other shapes andtcp_in_link=Noneunchanged. The bridge applies it to every segmented / fallback payload (#349: a mask that bled into the hand put a box against the carrying wrist).region_attachment(declaration, *, attach_link, touch_links, t_link_from_region, stamp_ns, support_z=None, extrinsic_error_m=MAX_PLANAR_ERR_M, tcp_in_link=None, inner_region=None) -> AttachedCollisionObject(L740) — The declaration's latched region as one box primitive posed in the attach link; giveninner_region(the measured, unpadded region), it rides as the box's stage-2tight_geometry(clipped like the box, clamped inside it) so the kernel checks the payload against the robot's own links as the measured box; giventcp_in_link, a box axis within 30° of the approach is shortened so nothing lies behind the tool point (#349: the margin's top bloat reached the carrying wrist);object_id= the declaration'sobject_id(the kernel handover's match key) or itstarget_id;AttachmentEvidenceKind.GRASP_TARGET_REGION, confidence 0.7.ROSConfigErrorwithout a region. Givensupport_z(the support top the grasp-target leg measured under exactly this region,GraspTargetLeg.measured_support), the payload carries the ADR-0092 D6SupportContactWitnesson that plane (_place_target_leg.plane_witness,support_id="map_support_under:<target_id>", stamped at the ATTACH, patch = the published box's horizontal footprint) — Isaac i42's payload, still on its table, was stopped 90 ms after the handover;Noneattests nothing. The bridge passes the region's cell closure (GraspTargetLeg.kernel_region), so the target's own boundary cells are embedded residue at the kernel's attach-time snapshot (Isaac i50).box_gap_lower_bound_m(a, b) -> float(L488) — Separating-axis (15 axes) lower bound on the distance between two oriented(centre, rotation, half_extents)boxes; exact on a face normal, never above the true distance,<= 0on overlap. The release window's separation test. Not a kernel mirror (see14-duplication-watch.md"Convex distance"): it only has to be a sound lower bound.freeze_released_attachment(held, *, base_link, t_base_from_link) -> AttachedCollisionObject(L529) — The released payload re-expressed on the collision model's root link at its DETACH pose: geometry, evidence and force attestation kept,support_contactdropped (the held payload's own is the grasp-timemap_support_under:*witness, a claim about the ATTACH pose that would ride the record misplaced through the window; the place leg's set-down witness is decorated per publish,PlaceTargetLeg.decorate, and still rides the record);touch_links= the held attach link + its touch links (nothing the held record did not already exempt);evidence_refsuffixed|frozen_release. Root, not hand: the kernel places a record atlink_world[attach_link]in every predicted configuration, so a hand-attached record would follow the retreat.primitive_poses(obj, t_frame_from_link) -> list[NDArray](L881) —frame <- primitive(4, 4) for each primitive of anAttachedCollisionObject, givenframe <- attach link; shared byReleaseWindow.clearance_m(the frozen record, link = base),GraspTargetLeg.on_attach's segmented-payload test, and_place_target_leg(payload_witness, andPlaceTargetLeg's posed published payloads each tick; the old private_posed_primitivescopy was removed).class ReleaseWindow(L895) — Frozen dataclass(record, opened_s, hand_link, hand_boxes, jaws)— one leg's released payload (design note §2.3 "Release").open(description, held, *, base_link, t_base_from_link, now_s) -> ReleaseWindow(L926) — Freezeheldand collect the hand's and its jaws' manifest collision primitives (bounded as boxes); aMimicJointfollower of a jaw joint hinged on the same hand (the OpenArm's second finger, its own link since #349) is a jaw too.clearance_m(t_base_from_hand, positions) -> float | None(L979) — Smallestbox_gap_lower_bound_mover (exempted link primitive, payload primitive); jaws posed byorigin · joint motion(q), a mimic follower atvalue_atof its leader's angle.Nonewhen a jaw angle is unknown — never "clear".close_reason(*, now_s, clearance_m, clear_m, timeout_s) -> str(L1022) —"timeout","separation"(strictly> clear_m), or""._release_base_link(description) -> str(L1052) —base_frame, proven the root of joints + fixed attachments, elseROSConfigError._tree_links(description) -> tuple[set[str], set[str]](L1043) —(parents, children)of every joint and fixed attachment; also thetf_frameskey check.bounding_half_extents(shape) -> NDArray(L411) — Half extents of the box containing a box / sphere / capsule primitive (distances on it are lower bounds); public for the grasp leg's HZ-0115-11 guard._xyz_rpy_matrix(xyz, rpy) -> NDArray(L437) —(4, 4)from translation + URDF fixed-axis RPY._joint_motion(joint, q) -> NDArray(L452) — Revolute/continuous (Rodrigues) or prismatic motion after the joint origin; any other type isROSConfigError.
python/hal/src/openral_hal/_grasp_target_leg.py
The pre-grasp target producer (design note docs/reference/real-pick-place-design.md §2.2), owned by VisionAttachmentBridge (single /openral/attachment_state authority), default off. Subscribes /openral/grasp_declaration (RELIABLE + TRANSIENT_LOCAL, depth 1) and reads the bridge's one shared /openral/world_voxels grid (VisionAttachmentBridge._on_voxels); at grasp_target_rate_hz measures the support plane from the map (support_top_from_voxels over search_column(box, below_m=support_search_below_m), ring probe support_probe_margin_m around the target's footprint, counted on the column grown by that margin + one cell — never the box's bottom face, a lifted bbox min-z that can sit below the table top; none is a no_support contradiction), seeds from the occupied cells of that column above it (whose lowest cell must sit within one voxel + one of tolerance of the support, else a not_on_support contradiction — a bench under the shelf board the target stands on; the mask fit refuses the same way when the masked cloud ends > 2 voxels above the support — a target stacked on another object), sends the seed as the single positive SegmentInView point (optical frame, no negatives, the leg's own deadline), fits target_region_from_mask on the depth frame the request was made against, cross-checks region_covers_occupied and gates track_region at one voxel. Contradicting evidence (no_support, not_on_support, ambiguous, caps, no_height_above_support, map_disagrees on any re-fit the map does not cover, target_moved on a re-fit reaching outside the held region grown by one voxel, unoccluded_refit on one shrunk inside it with no declared contact link near, frame/calibration mismatch) retracts at once; a lost view (no seed cells, seed off-image, no mask, a mask more than mask_depth_max_skew_s from its depth frame (mask_depth_skew), deadline, too_few_points, stale or missing grid/depth/TF/CameraInfo, hand_at_target — a fit refused outright with a region held and the hand over it (_hand_over_target: the jaw's hand point over the held footprint up to approach_m above its top, the measure occluded_refit uses too) — and partial_fit — a fit passing every other gate whose kernel (cell-closed) region leaves an occupied cell of the target's map component outside (occupied_touching_outside: more than a voxel above the measured support, touching the region's cells) and that the map cannot complete (map_completed_region inside the armed search column refused: the component touches the column's edge, exceeds the caps or leaves another body in the box; raised only where the fit would be accepted, never re-classing another refusal; a fit the map does complete is held, tracked, handed over and published as the completed box, Isaac i45) — and occluded_refit — a map-covered re-fit failing the tracking gate but inside the held region grown by one voxel while a declared contact link's hand point (VisionAttachmentBridge.jaw_point: its leg's TCP via the attach link, since jaw links such as OpenArm's finger_pair are not tf frames) is within one voxel + occluder_margin_m of it, the robot's own hand occluding part of the target; it never replaces the held region) freezes the last accepted region for grasp_target_freeze_s from its stamp_ns (the older of the depth stamp and the grid's source_stamp, so a stalled octomap whose bridge republishes a fresh header.stamp ages the region out), then retracts. Each transition logged once. A grid received longer than grid_max_age_s ago (the deploy's kernel voxel deadline, grid_stale), with no source_stamp (grid_source_unknown) or whose source_stamp is older than the freeze (grid_source_stale) is not used — lost views; the freeze defaults to twice grid_max_age_s and may not exceed it (the kernel's grasp_region_max_age_s = 2 × its voxel deadline). Approach-armed target (approach_m, default off): a live declaration with no search_box (the runner's goal-scope one, or a named one without a box) is measured where ONE hand approaches — each tick the hands (openral_core.gripper_hands) whose links the declaration names are located (jaw_point), approach_box around each; a hand with ≥ grasp_target_min_cells occupied cells in its box above the box's lowest occupied layer (approach_cell_count: the table under the hand is not a target) and holding nothing (no payload, release window or segmentation on its legs) is approaching; exactly one arms target_id="approach:<first link>:<n>" (n the tracker's pick counter, seeded from the wall clock at construction — GraspTargetTracker(first_pick=None) — and advanced per completed pick, so a re-activated HAL never reuses an identity), contact_links = that hand, the box as search_box, the goal's attribution/stamp/timeout; two at once arm none; the box follows the TCP; the hand leaving retracts at once (approach_ended) — except the armed hand while its legs hold or segment (its ATTACH resolves the arming). A grasp-leg request in flight when the tracker's generation moves (handover, re-arm, retraction, declaration change) is discarded on reply or deadline. A retracted arming (before any handover) re-arms for that hand only once its box centre moved more than one voxel or one freeze window elapsed (no arm-refuse-re-arm flood), under the same identity — except a hand that left the approach distance and was then seen away (located, its box off the last target's column) continuously for a freeze window, which arms under a fresh identity (liveness after a kernel grid_frame_changed / attachment_rejected retirement, which the producer cannot see). Every publish whose attachment set lost or replaced an object, or is empty, first drops every pre-handover arming's region and re-arms it under a fresh identity, accepting only a region measured after the change (on_attachment_changed; the kernel retires an armed declaration at every detach edge) — except that a hand mid-grasp (an ATTACH being resolved: a segmentation in flight or jaws loaded with nothing latched, GraspTargetLeg._resolving; a carried payload or release window is not mid-grasp) keeps its identity across a change that leaves the set non-empty, though still not its region, so its ATTACH is handed over with no region (fail closed); an emptying change refreshes it regardless. Handover is per hand: only an ATTACH on the armed hand (or a named search_box declaration's link) hands over, and only an ATTACH on what was measured (GraspTargetLeg.on_attach) hands the region over — else attach_off_target (handed over with no region; also when confirm raises). Multi-pick per goal: nothing arms while a hand is handed over; the bridge's DETACH (GraspTargetLeg.on_detach, with the release window's frozen record) drops that hand's region at once and keeps the handover (GraspTargetTracker.on_release); once the hand's legs hold nothing — re-checked on the DETACH, the window's close and every barrier release (on_hand_settled), never polled — the pick completes (on_pick_complete): the counter advances and the hand re-arms behind the backoff, but not until the column its next measurement would search (search_column of its approach box, support_search_below_m) clears the payload it just released by more than one voxel (_near_spent: box_gap_lower_bound_m against each primitive's bounding_half_extents box; no record = no re-arm for the rest of the goal; HZ-0115-11). A guarded hand is not a candidate: it does not count toward "two hands at once" (HZ-0115-12 is the kernel's one-hand rule). The release poll and a re-ATTACH are serialized by the bridge's one lock and a loaded trigger counts as holding, so the poll racing a re-ATTACH never completes the live regrasp's pick in either ordering. The kernel retires every pick's identity at its release and never re-arms one (HZ-0115-3). A named declaration stays handed over after its pick. A named search_box wins. Threading: every leg entry point (tick, reply, deadline, declaration, teardown) and bridge hook runs under the bridge's one re-entrant lock (the bridge's ATTACH runs on the HAL's proprio thread); the tracker's own RLock nests inside it. Only pure work on a snapshot runs outside — the tick's column scan (_seed) and the reply's decode/back-projection/gates (_measure, given the grid, hand points and held region read under the lock) — and is committed under the lock only under the snapshotted tracker generation; teardown sets a torn-down flag under the lock that _tick / _on_deadline check, so no request or deadline timer outlives it.
class GraspTargetTracker(*, freeze_s, log, first_pick=None)(L383) — Pure state machine on a caller-supplied ROS-clocknow_ns; thread-safe (every method under its own re-entrant lock, which in the HAL always nests inside the vision bridge's lock).first_pickseeds the pick counter inapproach:<link>:<n>;Noneseeds it fromtime.time_ns()so a re-activated HAL never re-mints a retired identity.on_declaration(declaration) -> None(L539) —active=Falseclears; a new(target_id, stamp_ns)resets the region; the latched copy again is a no-op.rebase(resolve) -> None(L573) — Re-expresses dispatch'ssearch_box/workspace_boxthroughresolve(field, box)from the boxes as declared (never compounding); identity, generation and region kept. The leg passesrebase_boxinto the grid frame every tick, with tf2 at the grid'ssource_stamp(VisionAttachmentBridge._lookup(stamp_ns=)), so the box sits where the base was when the map was built; no tf2 at that time leaves it unresolved (fail closed), logged once per field and state.wants_approach(*, now_ns) -> bool(L604) — Approach detection should run: a live declaration with nosearch_box, not handed over.on_approach(near, *, now_ns, move_m, holding=(), generation=None, located=None) -> None(L615) — Givengeneration(the tick's), ignored when the tracker moved on since (a pick completed on the HAL's thread).near=(hand links, approach box)per approaching hand this tick; only hands the declaration names count. Holds one hand: arms on exactly one, follows its box, retracts at once (approach_ended) when it leaves — unless it is inholding(legs holding, releasing or segmenting: arming and box kept for the ATTACH); two at once arm none (a backed-off hand still counts). A hand whose arming was retracted re-arms only after its box centre moves >move_morfreeze_selapses, under the same identity — unless its arming ended by leaving (approach_ended) and every sample for a continuousfreeze_ssince showed it inlocated(the approach boxes of the hands located and holding nothing this tick) with its box off the column over its last target: then a fresh identity (liveness after a kernel fault retirement the producer cannot see). An unlocated, holding or back-over-the-target sample restarts that window; absence fromnearalone is not away.on_attachment_changed(*, now_ns, emptied=True, holding=()) -> None(L882) — The bridge is publishing a set that lost or replaced an object, or is empty (the kernel's detach edge retires an armed declaration before its handover too). An arming not handed over drops its region and, approach-armed, advances to a fresh identity — the counter also advances when an identity was armed since the last advance and has since retracted (the kernel may still hold it); no region withstamp_ns <= now_nsis accepted afterwards (any arming). An arming whose hand is inholding(mid-grasp: an ATTACH being resolved) keeps its identity unlessemptied(the kernel retires on the empty edge regardless, so that grasp then hands nothing over) — but always drops its pre-change region, so its handover carries none. A handed-over pick follows its own path; a named declaration keeps dispatch's identity (the kernel refuses it once retired).presence_unknown() -> None(L767) — A tick that could not sample the hands (no fresh grid, a request in flight, a stale verdict): every away window restarts — unknown is not away.on_attach(contact_link, *, confirm) -> None(L786) —confirmreceives the held region orNone; Per hand: a link of the armed declaration (approach-armed hand, or a namedsearch_boxone) attached → no further measurement or approach arming until that hand's pick completes (on_pick_complete); the held region is kept for the kernel's handover only ifconfirm(region)(run under the lock, before any state changes) says the payload is its target, else dropped (attach_off_target, also whenconfirmraises). An ATTACH on a hand with nothing armed (the goal-scope declaration names every hand) hands nothing over and is logged.on_release(hand, record) -> None(L855) — The bridge's DETACH on the handed-over hand: drop its region at once, keep the handover (nothing re-latches it), rememberrecord(the frozen release record, orNone) as the hand's spent payload. A namedsearch_boxdeclaration drops its region too (nothing spent recorded: it never re-arms); a hand not handed over is untouched.on_pick_complete(hand, *, now_ns) -> bool(L946) — The handed-over hand holds nothing: drop the arming (backoff as a refusal), advance the pick counter, release the handover (spent defaulted toNonewhen noon_releasecame). Namedsearch_boxdeclarations stay handed over (returnsFalse).spent(hand) -> tuple[bool, AttachedCollisionObject | None](L976) —(guarded, released record)of the hand's last completed pick this goal; cleared by a new or cleared declaration.clear_spent(hand, record) -> None(L981) — End the guard, only whilerecordis still the guarded one; the ending is logged once (the hand may arm again).measured() -> tuple[GraspDeclaration | None, int](L483) —(target, generation)read atomically, to tag one measurement request.- (property)
generation -> int(L507) — bumped on every change of what is measured (declaration, arming, retraction, handover); the leg tags each request with it and drops a reply or deadline under an older one. - (property)
handed_over -> tuple[str, ...] | None(L494) — links of the hand whose ATTACH ended re-measurement. - (property)
region_generation -> int(L499) — thegenerationthe held region was accepted under; a map re-stamp of the same measurement keeps it, a new declaration or pick bumps it. wants_measurement(*, now_ns) -> bool(L988) — Live, has asearch_box, not handed over.accept(region, *, generation=None, map_cells=0) -> None(L999) — Hold a region (map_cells> 0: completed from the map — that many cells came from the map alone, logged with the completed half-extents on entering that state); one that breaksGraspDeclaration's caps is refused (declaration_bounds). Ignored once handed over, under agenerationother than the current one, or when the region was measured at or before the last attachment-set change (on_attachment_changed;measured_before_change, logged once).refuse(kind, detail, *, retract, now_ns, generation=None) -> None(L1049) — Retract now, or freeze under the TTL. Ignored once handed over, or under agenerationother than the current one.envelope(*, now_ns) -> GraspDeclaration | None(L1085) — Dispatch's fields verbatim + the held region; expiry clears, an over-age unfrozen region is retracted.- (property)
declaration -> GraspDeclaration | None(L473) — dispatch's live copy, region-less. - (property)
target -> GraspDeclaration | None(L478) — what is measured and published: the approach-armed one-hand declaration while held, else dispatch's. - (property)
region -> PlaceRegion | None(L489) — the last accepted region still held. APPROACH_TARGET_PREFIX = "approach:"(L322) —target_idprefix of an approach-armed declaration; the hand's first link and:<n>(the pick counter) follow — one identity per pick.approach_cell_count(grid, box) -> int(L1242) — Occupied cells in an approach box above its lowest occupied layer (the support under the hand); one layer only, so a box bottom cutting a small object still counts it.approach_box(points, *, approach_m, frame_id, cap_m=GraspDeclaration.MAX_HALF_EXTENT_M) -> PlaceRegion(L1206) — Gravity-aligned box spanning a hand's TCP points grown byapproach_m, each half-extent capped atcap_m(the pre-measurement box lifts it); the approach test and the approach-armed target's search box.ROSConfigErroron no points.lattice_from_msg(msg) -> VoxelLattice(L1106) —openral_msgs/OccupancyVoxels→VoxelLattice.rebase_box(box, t_frame_from_box, frame) -> PlaceRegion(L1129) — A gravity-aligned declared box re-expressed inframethrough tf2frame <- box, still yaw-only (the transform's tilt, at most 0.5°, dropped; more, or a tilted box, is aROSConfigError).GraspTargetLeg._tickapplies it every tick to asearch_box/workspace_boxa scene declares in a fixed frame (odom,map) viaGraspTargetTracker.rebase(resolve)— which re-expresses the boxes as declared, never compounding, keeping identity, generation and region — so they stay put while a mobile base moves; with no tf2 the box stays in its own frame and the existingframe_mismatchrefusal / workspace retraction applies.search_column(search_box, *, below_m) -> PlaceRegion(L1174) — The search box extendedbelow_mdownward, the column the support is measured in; a tilted box is aROSConfigError._hand_over_target(hands, held, *, reach_m, hand_rise_m) -> bool[private] (L1303) — A declared contact link's hand point (the jaw TCP, a finger length above the tips) over the held region's footprint, up tohand_rise_mabove its top, grown byreach_m; the measurehand_at_targetandoccluded_refitshare._refused_fit(kind, detail, previous, hands, *, reach_m, hand_rise_m, visible=None, tol_m=0.0) -> _Refusal[private] (L1352) — A refusal fromtarget_region_from_masktyped for the tracker:too_few_pointsis a lost view; anot_on_supportwith a region held, itsvisiblebox (TargetRegionFit.box) inside the held region grown bytol_m(one voxel) and the hand over it (_hand_over_target) is thehand_at_targetlost view (the hand occluding the target's lower part, Isaac i25; the held region stays under its freeze TTL, never replaced); every other refusal (no height, the caps) and a visible part reaching outside the held region (target knocked over or gone) is the contradiction it names, hand or not._within_caps(region, grid) -> bool[private] (L1390) — Whetherregion's cell closure fitsGraspDeclaration's half-extent and volume caps unclamped._capped_margin(region, grid, margin_m, *, down=True) -> tuple[float, str][private] (L1398) — The largest margin<= margin_mwhose bloat (margin_grown_region(down=down)) still fits the caps once cell-closed — the margin shrinks, never the measured box (bisection, rounded down; noted when reduced,0when the unbloated closure is already at the caps). The note names the state, never the bisected value, so logging on change logs once per state._kernel_closure(region, grid, *, margin_m=0.0, down=True) -> tuple[PlaceRegion, str][private] (L1428) — The region as the kernel gets it overgridand a note when it is not as configured: bloated by the capped margin (margin_grown_region, every face withdown, sides and top without), thencell_closed_regionclamped atGraspDeclaration.MAX_HALF_EXTENT_M(noted), the tight fit when the closure exceedsMAX_VOLUME_M3(noted).margin_m=0is the unbloated closure ofregionexactly. Used bykernel_region(published region and payload, with the capped margin) and, unbloated, by_gate_refit(what the whole-target gate checks)._gate_refit(grid, region, previous, *, support_z, min_cover, column, hands=(), occluder_margin_m=0.05, hand_rise_m=0.0, complete=True) -> tuple[PlaceRegion, int][private] (L1478) —(accepted, added). Map cover on the fit (map_disagrees); the whole-target gate: whenoccupied_touching_outsideon_kernel_closure(region)is non-empty the fit is completed from the map (map_completed_regioninsidecolumn, Isaac i45) and the completed box is the candidate — what is tracked, held, handed over and published (cell-closed); then the tracking gate on that candidate (target_moved/occluded_refit/unoccluded_refit), so a later fit of more or less of the target completes or fits to about the same box. Against a held region (previous) the leg only logs its outcome: a held region is frozen and only the map retracts it (GraspTargetLeg._map_hold, design Revision 4). A refused completion leaves the tight fit as the candidate and, where it would otherwise be accepted, raises the lost viewpartial_fit(never re-classing another refusal; nothing new accepted, a held region kept under its freeze TTL).added= the cells the map added (0 for a fit holding the whole component).WORKSPACE_MIN_COVER = 0.10(L355) — The fraction of a declaredworkspace_box's footprint that occupied cells centred in it must cover for the leg to confirm it (region_covers_occupied); a calibration point (issue #349).class GraspTargetLeg(node, bridge, config, *, support_search_below_m=0.15, support_probe_margin_m=0.05, occluder_margin_m=0.05, approach_m=None, premeasure=False)(L1612) — ROS wiring;premeasure(fromVisionAttachmentConfig.grasp_target_premeasure, default off) pre-measures object instances before any hand arms: everySegmentInViewrequest is then an instance prompt (_premeasure: at most_PREMEASURE_MAX_HZ= 2 Hz, one in flight, around each free hand within_PREMEASURE_REACH_APPROACHES= 2 approach distances or in the named/armed box;instance_promptor a refresh of an instance older than half a freeze, every instance in the armed box due each prompt), its reply fitted by the camera path's_fitand kept only whenmap_confirmsit (_on_instance_reply→PrimitiveTracker); instances older thanfreeze_sor no longer covered by the map are dropped every tick. The armed region comes from the instances with no segmenter call (_tick_instances: with nothing held, the instance nearest the armed hand, refused on a tie, or the lone one in a named box, through_gate_instance— its whole region inside that box plus one voxel,map_confirms,_gate_refit(complete=False)— then accepted). Frozen once held (design Revision 4, both paths):_map_hold(grid, held, source_ns, hands)every tick —map_confirmson the held box and its measured support;map_disagreesretracts,map_splitretracts unless every second body lies in a declared hand's own space (_hand_boxes, passed asmap_confirms(own=…); then a lost view, the freeze TTL runs), confirmed re-stamps the same box with the grid'ssource_stamp; re-fits still run but their outcome is only logged once per change (_refit_ignored:grasp target re-fit ignored, region frozen (map-confirmed) — …); on the camera path_hold_frozenruns_map_holdeach tick and_refuse_refitlogs instead of applying every re-fit refusal (seed, request, deadline, reply) while a region is held;approach_m(fromVisionAttachmentConfig.grasp_target_approach_m,None= off) turns on the approach-armed target and must be in(0, GraspDeclaration.MAX_HALF_EXTENT_M]; reuses the bridge's depth/CameraInfocaches, tf2 buffer andSegmentInViewclient.ROSConfigErroron a rate outside 2-5 Hz, a non-positive cell count / cover / probe margin, a freeze outside(0, 2·grid_max_age_s](the kernel'sgrasp_region_max_age_s), a search depth outside(0, 0.5]m or an occluder margin outside[0, 0.10]m (upper bounds keep the gates meaningful), orconfig.grasp_target_margin_moutside[0, VisionAttachmentRuntime.MAX_GRASP_TARGET_MARGIN_M]; the setup line logs the margin. The bridge passes the support and occluder tunables fromVisionAttachmentConfig.grasp_target_support_search_below_m/_support_probe_margin_m/_occluder_margin_m; the keyword defaults serve direct construction.setup() -> None(L1779) — The declaration subscription + the measurement timer (the grid is the bridge's).- (attribute)
unfiltered_fits -> int— Consecutive fits that found no self-filtered cloud for their capture althoughself_filtered_cloud_topicis set (the self-filter dropped it, the 8-deep cache rolled over, or no tf2): the capture is then not fitted (lost viewunfiltered), since the robot's own points would enter it. Entering that state logs one warning (topic, depth stamp, cache size and stamp span); the next filtered fit logs the count and resets it (CLAUDE.md §1.4). _kept_points(stamp_ns, frame) -> NDArray[np.float64] | None(L3254) —VisionAttachmentBridge.kept_pointsfor_measure, keepingunfiltered_fitsand its two log lines.teardown() -> None(L1819) — Idempotent, under the bridge lock; marks the leg torn down and cancels any in-flight request's deadline.fill(msg, *, now_ns) -> None(L1835) — Setsgrasp_declaration_valid/grasp_declarationon oneAttachmentState; the region goes out cell-closed (kernel_region): before the handover bloated on every face, from the handover on (tracker.handed_over; the bridge'son_attachprecedes the ATTACH snapshot the kernel latches from) the payload box (payload=True), so the latched region is the payload — no downward bloat after the handover, the payload origin inside it at every margin. The tracker keeps the tight fit. Since #349 the declaration also carriesworkspace_region=published_workspace(...)when the leg holds a confirmation of itsworkspace_box.published_workspace(declaration, *, now_ns) -> PlaceRegion | None(L2099) — The confirmed manipulation workspace (issue #349) to publish withdeclaration: only the confirmation of its ownworkspace_box(same frame, pose, extents; never grown) and only withinfreeze_sof its stamp (the confirming grid'ssource_stamp)._confirm_workspace(now_ns)(every tick, under the bridge lock) confirms the live declaration's box on a fresh grid (_fresh_grid) when occupied cells centred in it cover ≥WORKSPACE_MIN_COVERof its footprint, retracts at once onmap_disagrees/frame_mismatch/ no box declared, and keeps the last confirmation on a stale or missing grid until it ages out; each state change is logged once.kernel_region(target_id, region, *, payload=False) -> PlaceRegion(L1866) — The held region as the kernel gets it (payload=False, before the handover: bloated bygrasp_target_margin_mon every face, downward too) or as the region payload's box (payload=True,VisionAttachmentBridge._region_payloadandfillfrom the handover on, Isaac i50: lowered onto the measured support top bylowered_to_support(Isaac i56/i57), then bloated on the sides and top only — the region the kernel latches) (HZ-0115-32), thencell_closed_regionover the newest grid, clamped atGraspDeclaration.MAX_HALF_EXTENT_M; a closure overMAX_VOLUME_M3, no grid or a grid in another frame publishes the tight box. The capped margin is cached per (region, grid frame/resolution/orientation, margin) per box kind, so an unchanged region costs no bisection per publish. Every such case is logged once per change of (box kind, note), never per refit near the caps.on_detach(jaw_link, record) -> None(L2597) — The bridge's DETACH (after it dropped the leg's attachment), with the release window's frozen record orNone:tracker.on_release, then the pick completes if the hand's legs hold nothing (no attachment, window, pending segmentation or loaded trigger). Event-driven, never polled.on_attachment_changed(*, emptied) -> None(L2613) — Called byVisionAttachmentBridge._publish_attachmentbefore it fills a snapshot whose set lost or replaced an object, or is empty:tracker.on_attachment_changedat the node clock with the links of every hand with an ATTACH being resolved (_resolving: a segmentation in flight or loaded jaws, nothing latched) — producer-side liveness for the kernel's detach-edge retirement, HZ-0115-3; a mid-grasp hand keeps its identity unless the set emptied, never its region.on_hand_settled(jaw_link) -> None(L2637) — The leg's release window closed (not for a re-ATTACH), its barrier was released, or it DETACHed with nothing latched: completes the pick if the hand holds nothing (a trigger reading the jaws loaded counts as holding). Event-driven, never polled.measured_support(region) -> float | None(L3040) — The support top (support_top_from_voxels) the request whose reply the tracker accepted as exactlyregionwas measured on (_on_replyrecords it;target_region_from_maskstood the box one voxel above it), inregion.frame_id;Nonefor any other region. What_region_payloadpasses toregion_attachment(support_z=…).on_attach(jaw_link, payload, *, region) -> None(L2651) — The bridge's one entry for a resolved ATTACH:regionset (the payload was built from it,_region_payload) hands over only while the tracker still holds exactly that region (dropped or replaced since →attach_off_target, never "absent");region=None(segmented,_finish) hands over only when the jaw is at the held region (VisionAttachmentBridge.jaw_at, reachoccluder_margin_m) and every payload primitive centre lies in it grown by one voxel — elseattach_off_target.
python/hal/src/openral_hal/_place_target_leg.py
The real place producer (design note docs/reference/real-pick-place-design.md §2.3; drafted, unapproved ADR-0097 / ADR-0092 D6 amendments in docs/reference/real-pick-place-adr-drafts.md), owned by VisionAttachmentBridge, default off. Nothing is surveyed and nobody has to name a place target, but nothing arms outside a goal: at place_target_rate_hz, only under a live dispatch /openral/place_declaration (else no_declaration), against the newest /openral/world_voxels (the bridge's shared grid; received, or its source_stamp data, older than grid_max_age_s, or source_stamp unset/future → grid_stale, a lost view), it measures the surface directly under the held, loaded payload — a column of the payload's lattice-axis footprint grown by one voxel + place_target_extrinsic_error_m, from its bottom down place_target_search_depth_m; the first occupied layer must cover the whole footprint (partial_support otherwise) with the payload's height + one voxel + the extrinsic bound free above it (no_headroom); cells within one voxel of any published payload are ignored. The patch is latched as a one-voxel slab region (grid frame, no geometry, stamp_ns = the grid's source_stamp — the world data's capture time, the kernel's own voxel data-age clock, never header.stamp, which a bridge republishing a stalled octree keeps fresh) and re-verified (not re-chosen) while the payload stays over it: new occupancy above it retracts at once, missing support cells / a missing or stale grid freeze it for at most place_target_freeze_s (default and ceiling 2 * grid_max_age_s, the kernel's place-region age bound) from its last verification. The dispatch /openral/place_declaration (RELIABLE + TRANSIENT_LOCAL, depth 1; region never relayed) is required — the runner's goal-scope one (place_approach_enabled, target_id="surface") or a named one; the leg never declares on its own. Its fields ride the envelope (the measured region attached, object_id narrowed to the measured payload when empty) and its search_box refuses a patch outside it (outside_hint). A retraction (goal end, cancel, E-stop) or expiry retracts the patch and the witness (no_declaration) and nothing re-arms until a new declaration; a new declaration drops a latched patch (redeclared) so it is re-measured under the new hint. The witness (MAP_SUPPORT_PROXIMITY, proximity to a map-measured plane, not sensed contact, not a proven support) is attested once per (declaration, payload); it dies with the region (contradiction, the freeze TTL = the kernel's region age bound, retraction or a payload change), so no part of the place allowance outlives a region the kernel would drop as region_stale.
class PlaceRefusal(StrEnum)(L139) — Typed reasons:no_payload,payload_changed,no_grid,grid_stale,tilted,no_surface,partial_support,no_headroom,outside_hint,frame_mismatch,free_volume_occupied,support_occluded,freeze_ttl,grid_moved(the lattice re-snapped since the latch — a contradiction),no_declaration(no goal: retraction, expiry or none yet),redeclared(a new declaration drops the old latch); the grid ones andsupport_occludedare lost views that freeze a latched patch.class PlacePatch(L168) — Frozen dataclass: latticelayer,i0/j0/ni/nj,free_layers,plane_z, the slabregion, and the latch grid'slattice((origin, orientation_xyzw, resolution), which the indices are into).measure_under_payload(grid, payload, *, extrinsic_error_m, search_depth_m, exclude=(), evidence_ref="") -> PlacePatch | tuple[PlaceRefusal, str](L297) — The surface under the carried payload (see the module note);region.evidence_refcarriesmap_measured_support:plane_z=…;patch=<ni>x<nj>@layer<k>.verify_patch(grid, patch, payload=()) -> tuple[PlaceRefusal, str] | None(L382) — Re-check on a newer grid:grid_moved(origin/orientation/resolution off the latch lattice by >_LATTICE_TOL= 1e-6 — the indices name other cells; contradiction),free_volume_occupied(contradiction) orsupport_occluded(lost view);None= holds.payload_witness(obj, t_base_link, patch, *, support_id, resolution, extrinsic_error_m, stamp_ns) -> tuple[SupportContactWitness | None, str](L441) — Lowest primitive point withinmax(resolution, extrinsic_error_m)of the measured plane and centre over the patch → the witness (plane_witness).plane_witness(obj, t_base_link, *, plane_z, support_id, extrinsic_error_m, detail, stamp_ns) -> SupportContactWitness(L485) — The one map-support proximity witness construction, shared bypayload_witness(place) andvision_attachment_bridge.region_attachment(vision pick): contact point under the payload's centre onz = plane_z, normal +z, both in the object frame;patch_radius_m= the horizontal footprint (per primitive: horizontal offset + its local box's farthest corner projected onto the plane — a yaw-only box'shypot(hx, hy), never the 3-D half-extent norm, whose vertical term widened the exemption) ≤ 0.5;max_penetration_m = min(extrinsic_error_m, 0.01);confidence=0.5;MAP_SUPPORT_PROXIMITY.class PlaceTargetTracker(*, freeze_s, extrinsic_error_m, log)(L560) — Pure state machine on a caller-supplied ROS-clocknow_ns.on_declaration(declaration) -> None(L635) — Dispatch's goal-scope copy;active=Falsedrops it with the patch and the witness (no_declaration); a new one drops a latched patch (redeclared) and the witness; the latched copy again is a no-op.latch(patch, *, payload_key, resolution) -> None(L671) — Hold a measured patch.reverified(grid_stamp_ns) -> None(L684) — Restamp the region with the verifying grid'ssource_stamp(an unchanged stamp never makes it younger).refuse(reason, detail, *, now_ns) -> None(L693) — Lost view freezes a latched patch; anything else retracts.live(*, now_ns) -> PlaceDeclaration | None(L714) — Dispatch's live declaration; expiry ends the allowance like a retraction.envelope(*, now_ns) -> PlaceDeclaration | None(L726) — Dispatch's live declaration (+ the held region, scoped to its payload when dispatch names no object), elseNone; expires an over-age region.attest(candidates, *, now_ns) -> bool(L744) — Arm or drop the witness from(attachment, T_grid_from_attach_link | None, loaded)per leg;Truewhen the published set must change.decorate(objects) -> list[AttachedCollisionObject](L803) — The set with the held witness on the object it names.- (property)
declaration -> PlaceDeclaration | None(L604) — dispatch's copy in force, region-less. - (property)
patch -> PlacePatch | None(L609) — the latched patch. - (property)
payload_key -> tuple[str, int] | None(L614) — the payload the patch was measured under. place_tick(tracker, grid, grid_stamp_ns, *, carried, published, extrinsic_error_m, search_depth_m, now_ns) -> None(L838) — One pure measurement tick (grid_stamp_ns= the grid'ssource_stamp): nothing without a live declaration (no_declaration); re-verify the latched patch while its payload is over it (or released onto it), else refuse a dispatch hint in another frame than the grid (frame_mismatch, before measuring), then measure under the carried payload, applying the hint (outside_hint).class PlaceTargetLeg(node, bridge, config)(L931) — ROS wiring; reuses the bridge's tf2 buffer, legs and publisher.ROSConfigErroron a non-positive rate / freeze / depth / extrinsic bound, or a freeze past2 * grid_max_age_s.setup() -> None(L999) — The declaration subscription + the measurement timer (the grid is the bridge's shared one).teardown() -> None(L1032) — Idempotent.fill(msg, *, now_ns) -> None(L1044) — Setsplace_declaration_valid/place_declarationon oneAttachmentState.decorate(objects) -> list[AttachedCollisionObject](L1051) —PlaceTargetTracker.decorate.on_joint_state() -> None(L1056) — Witness re-evaluation (throttled to 20 Hz); republishes on a change.
Node wiring (ManifestHALLifecycleNode): vision_attachment_* params (enabled defaults to False; vision_attachment_tf_frames takes "link=frame" strings; no trigger thresholds are params — they are the manifest's closure_calibration; _on_safe_action feeds the HAL's applied command to observe_command (_fold_applied_command, which clears it via clear_command when a committed group's command is unknown); the trigger windows are PositionStallConfig.for_rate of the node's resolved joint-state rate (publish_rate_hz, else the manifest's control_rate_hz), with vision_attachment_trigger_max_gap_s (default 0.0 = derived) as an explicit gap override; vision_attachment_grasp_target_support_search_below_m / _support_probe_margin_m / _occluder_margin_m (defaults 0.15 / 0.05 / 0.05 m) reach GraspTargetLeg through the matching VisionAttachmentConfig fields; vision_attachment_grid_max_age_s / _release_clear_m default 0.0 = unset and are REQUIRED with the leg on — the kernel's voxel deadline and margin + resolution, passed by deploy_e2e.launch.py; unset is a ROSConfigError naming the param at activate, no cell's values as fallback); built in _setup_vision_attachment on activate, torn down on deactivate/cleanup. _attachment_barrier_holders() / _attachment_perception_ready() require every present holder (sim bridge + vision bridge) to clear before a tick is acknowledged. _publish_action_applied_if_complete re-checks readiness right after storing a deferred tick (_on_attachment_perception_ready), so a holder settling on another thread between the check and the store cannot strand it (a lost wakeup); _publish_action_applied_tick's check-and-publish runs under _ack_publish_lock (calls no holder), so that re-check and a holder's notify ack a tick once.