Skip to content

ROS 2 Lifecycle Nodes (packages/)

Part of the OpenRAL public-symbol inventory. Hand-curated; (LNN) markers are refreshed by tools/refresh_methods_linenos.py.

Thin wrappers around the Python-layer adapters; each exposes a single main() entry point for ros2 run.

  • packages/openral_hal_so100/openral_hal_so100/lifecycle_node.pymain() -> None (L46) — SO-100 LeRobot HAL. ADR-0018 F8 heartbeat wired.
  • packages/openral_hal_openarm/openral_hal_openarm/lifecycle_node.pymain() -> None (L57) — Enactic OpenArm v2 bimanual HAL. Wraps openral_hal.OpenArmMujocoHAL (16-DoF). Subscribes to /openral/safe_action + /openral/estop; publishes /joint_states. ADR-0018 F8 heartbeat wired. Lifecycle integration coverage in tests/integration/test_openarm_hal_lifecycle.py (3 tests: 16-DoF publish path, safe_action → digital-twin actuation, estop latch).
  • packages/openral_hal_franka/openral_hal_franka/lifecycle_node.py — Franka Panda HAL.
  • packages/openral_hal_ur5e/openral_hal_ur5e/lifecycle_node.py — UR5e HAL.
  • packages/openral_hal_ur10e/openral_hal_ur10e/lifecycle_node.py — UR10e HAL.
  • packages/world_state/openral_world_state_ros/lifecycle_node.pymain() -> NoneWorldStateAggregator lifecycle node. ADR-0018 F2: publishes typed openral_msgs/WorldStateStamped on /openral/world_state_fast (publish_rate_hz_fast, default 30 Hz) and /openral/world_state_slow (publish_rate_hz_slow, default 5 Hz) from one in-memory snapshot per fast tick. JSON /world_state topic removed. Module exposes TOPIC_FAST, TOPIC_SLOW, and build_world_state_stamped_msg(node, world_state) -> WorldStateStamped (L849) — pure-Python translation of openral_core.WorldState to the typed message (sorted parallel arrays for determinism; trace_id set from current OTel context via openral_observability.propagation.current_traceparent). ADR-0035: build_world_state_stamped_msg now also serialises the spatial memory onto the detected_object_* parallel arrays (labels / confidences / geometry_msgs/Point positions / int32 track-ids (-1 = unset) / frame) via the private helper _fill_detected_objects(msg, world_state) -> None (L999), so separate-process consumers of /openral/world_state_slow now see WorldState.detected_objects on the wire. __init__ accepts an optional pre-constructed WorldStateAggregator so F1's compose factory can share one instance with rskill_runner_node (ADR-0018 §3 — single shared aggregator).
  • world_state_from_idl(msg) -> WorldState (L717) — Symmetric inverse of build_world_state_stamped_msg: reconstructs an openral_core.WorldState from a WorldStateStamped msg (joint/base/EE/diagnostic state plus, since ADR-0035, the detected_object_* parallel arrays → list[DetectedObject]). Used by the reasoner_node to feed real state into its ContextRenderer. image_frames is always None (the IDL carries image topic refs, not inline pixels).
  • packages/openral_perception_ros/openral_perception_ros/ros_image_detector_node.pymain(args=None) -> None (L51) — ADR-0035 standalone ROS-Image object detector (no GStreamer). Defines the node class lazily inside main() behind ROS imports: RosImageObjectDetectorNode(LifecycleNode) — subscribes a camera sensor_msgs/Image on image_topic, runs an openral_runner detector backend, and publishes openral_msgs/PromptStamped (carrying the detector's ObjectsMetadata as metadata_json) on output_topic (default /openral/perception/objects), header.frame_id stamped with sensor_id. Backend selection (_build_detector, ADR-0037 2026-06-09 amendment): with a manifest_path param set, builds via openral_runner...detector_factory.build_manifest_detector (RT-DETR ONNX for runtime: onnx, or the open-vocab LocateAnythingDetector / VLM_SIDECAR for runtime: pytorch); without it, the legacy ObjectsDetector (RT-DETR ONNX) from onnx_path + labels. Detector mode (ADR-0051)_resolve_wiring reads the manifest's detector.mode at on_configure (via detector_node_wiring): a continuous detector runs the primary camera's detect+publish leg and does NOT expose locate_in_view / subscribe detector_query; an on_demand detector exposes the service + query topic and does NOT publish continuously (frames still cached). Legacy ONNX path (no manifest) = continuous. Dynamic query (_on_query, on_demand only): an initial query param is applied and the node subscribes a std_msgs/String query_topic (default /openral/perception/detector_query) to retarget the persistent query live. Camera-agnostic (ADR-0043): a cameras param maps logical ids → image topics (falling back to a single image_topic under primary_camera); every camera's latest frame is cached (_cache_frame), so no camera name is baked in. locate_in_view service (ADR-0043, on_demand only): serves /openral/perception/locate_in_view (openral_msgs/srv/LocateInView) — a read-only one-shot detect of a requested query on a requested camera's cached frame (_on_locate_in_view, uses detect_with_query so the persistent query is untouched); offered only for on_demand detectors with the IDL built. Params: cameras, primary_camera, image_topic, output_topic, sensor_id, onnx_path, manifest_path, model_id, score_threshold, input_size, max_rate_hz, labels, query, query_topic. Lifecycle (ADR-0050)RosImageObjectDetectorNode(LifecycleNode): the GPU detector backend is built on on_activate and released on on_deactivate/on_cleanup (_release_detector) so the reasoner can free its VRAM before a co-resident grab policy loads. Continuous-leg observability (issue #12)_detect_and_publish never swallows a per-frame outcome silently: classify_continuous_tick(*, error, detection_count) -> tuple[str, str] (L63, module-level, pure) maps a tick to a (log_level, message) — a detect() exception → warning (a crashing/OOM detector must be visible, not hidden, since the ADR-0035 "publish nothing when nothing is seen" contract makes a silent crash look identical to a quiet scene), an empty result → info liveness heartbeat, a non-empty result → debug; the node logs it via _log_throttled (throttle_duration_sec=5.0) without changing what lands on the bus. DEBUG-on-demand_apply_env_log_level (on_configure) honours the DETECTOR_LOG_LEVEL_ENV (OPENRAL_DETECTOR_LOG_LEVEL) env var via normalize_log_level(value) -> str | None (L83, pure: case-insensitive, warningWARN, unknown→None) + rclpy.logging.set_logger_level, so OPENRAL_DETECTOR_LOG_LEVEL=debug openral deploy sim … surfaces the per-publish DEBUG line the default INFO console hides (the wrapped launch can't easily inject --ros-args --log-level). Best-effort producer. Launched via openral deploy sim --enable-object-detector (--object-detector-manifest selects the VLM); the reasoner offers locate_in_view when detector_available is set.
  • packages/openral_perception_ros/openral_perception_ros/scene_vlm_node.pymain(args=None) -> NoneADR-0047 scene-VLM query service node. Defines SceneVlmNode(Node) lazily inside main() behind ROS imports: subscribes one or more camera sensor_msgs/Image streams, caches each camera's latest BGR frame (_make_cache_cb), and builds a QwenSceneVlm backend from a kind:"vlm" manifest_path (_build_vlmbuild_scene_vlm). query_scene service (ADR-0047): serves /openral/perception/query_scene (openral_msgs/srv/QueryScene) — read-only, on-demand "answer this question about camera Y's current frame" (_on_query_sceneQwenSceneVlm.query, returns free text); offered only if the IDL is built. The scene-reasoning counterpart of ros_image_detector_node (which serves locate_in_view) — separate node because a scene VLM is a reasoning aid, not a continuous detector. Params: cameras, primary_camera, image_topic, manifest_path (required), sidecar_host, sidecar_port. The reasoner offers query_scene when scene_query_available is set.
  • packages/openral_perception_ros/openral_perception_ros/reward_monitor_node.pymain(args=None) -> NoneADR-0057 reward-monitor query service node. Defines RewardMonitorNode(Node) lazily inside main(): subscribes the co-active VLA's camera sensor_msgs/Image topic(s), buffers frames per camera into a RollingFrameBuffer (downsampled to the manifest's target_fps, stamped on the node clock so eviction/staleness work in sim + real), and builds a RobometerReward backend from a kind:"reward" manifest_path (_build_monitorbuild_reward_monitor). query_task_progress service (ADR-0057): serves /openral/perception/query_task_progress (openral_msgs/srv/QueryTaskProgress) — read-only windowed progress/success assessment (_on_query_task_progressRobometerReward.assess); returns ok=False, stale=True when no fresh frame / no task. The reward counterpart of scene_vlm_node (the rolling buffer lives node-side; the sidecar is a stateless scorer). Params: cameras, primary_camera, image_topic, manifest_path (required), task, sidecar_host, sidecar_port. The reasoner offers query_task_progress when task_progress_available is set. Advisory-only — no actuation.
  • packages/openral_perception_ros/openral_perception_ros/image_convert.pyimage_to_bgr_bytes(msg) -> tuple[bytes, int, int] (L14) — Converts a sensor_msgs/Image (rgb8/bgr8, tightly-packed rows) to contiguous H·W·3 BGR uint8 bytes plus (width, height) for ObjectsDetector.detect; rgb8 is reversed to BGR, bgr8 passes through. Raises ImageConvertError (L10) on an unsupported encoding or a padded row stride (step != width*3). No cv_bridge dependency.
  • packages/openral_safety/openral_safety/supervisor_node.pySafetyPassthroughNode(node_name="openral_safety") (L57), main(args=None) (L341) — ADR-0018 F5 Day-1 pass-through. Lifecycle node owning /openral/candidate_action → /openral/safe_action plus /openral/estop + /openral/estop_reset (std_srvs/Trigger). Day-1 envelope checks: n_dof mismatch and per-joint position-limit on the first chunk row. SafetySupervisorNode is a back-compat alias. The constant DEFAULT_ESTOP_RESET_COOLDOWN_S = 0.5 seeds the estop_reset_cooldown_s parameter.

All four HAL lifecycle_node.py files share the same shape: import the matching Python HAL class and call openral_hal.lifecycle.make_lifecycle_main(...). The generic wrapper at python/hal/src/openral_hal/lifecycle.py ships the F8 heartbeat, the /openral/safe_action consumer (ADR-0018 F1) and the /openral/estop latch (ADR-0018 §F5) for franka / ur5e / ur10e.

  • packages/openral_rskill_ros/openral_rskill_ros/rskill_runner_node.pyRskillRunnerNode(*, node_name="openral_skill_runner", robot_description, aggregator, skill_resolver=None) (L95), main(args=None) -> int (L575) — ADR-0018 F1 ExecuteRskill action server. Lifecycle node that owns /openral/execute_rskill (openral_msgs/action/ExecuteRskill), constructs an openral_runner.ROSPublishingHAL, subscribes to /openral/estop defense-in-depth, and emits the F8 heartbeat. SkillResolver is the injected resolver type that returns a configured + activated rSkillBase; production uses make_default_skill_resolver(self) (ADR-0024 — branches on manifest.kind to route VLAs to the local/HF-Hub path and ros_action/ros_service skills to ROSActionRskill). The execute loop catches ROSRskillGoalSatisfied specifically and closes the goal with success=True.
  • make_default_skill_resolver(ros_node, *, search_paths=(), scene_cameras=()) -> SkillResolver — Production resolver factory that captures the host lifecycle node so wrapped-ROS rSkills can build their ActionClient / service client on it. Inspects manifest.kind: "vla"make_local_skill_resolver (when in-tree) or _default_skill_resolver (HF Hub); "ros_action" / "ros_service" → instantiates ROSActionRskill; "wam" → raises ROSConfigError (not implemented in ADR-0024).
  • make_local_skill_resolver(search_paths, *, scene_cameras=()) -> SkillResolver — In-tree VLA resolver. Walks each search path once and indexes every */rskill.yaml by manifest name; on resolve calls _build_runtime_skill_from_manifest. Accepts ros_node=None kwarg for signature uniformity with the default resolver.
  • packages/openral_rskill_ros/openral_rskill_ros/compose.pycompose_runtime(robot_yaml, *, skill_resolver=None) -> ComposedRuntime (L69) loads any in-tree robots/<id>/robot.yaml via RobotDescription.from_yaml, builds a single WorldStateAggregator, and hands the same reference to a colocated _WorldStateLifecycleNode and RskillRunnerNode. Honors ADR-0018 §3 "only subscriber of /joint_states." compose_so100_runtime(*, skill_resolver=None) (L120) is the SO-100 convenience wrapper. Returns a ComposedRuntime dataclass with description, aggregator, world_state_node, rskill_runner_node fields.
  • packages/openral_rskill_ros/scripts/runtime_node — Composed-runtime entry point installed as lib/openral_rskill_ros/runtime_node. Reads ROS parameter robot_yaml (absolute path), calls compose_runtime, and spins both lifecycle nodes on a MultiThreadedExecutor. Spawned once per openral deploy sim invocation by sim_e2e.launch.py (one generic launch — the per-robot openarm_e2e.launch.py / so100_e2e.launch.py were unified into sim_e2e.launch.py in 2026-05-24).

python/runner/src/openral_runner/ros_publishing_hal.py

ADR-0018 F1 — HAL Protocol adapter that publishes ActionChunk on /openral/candidate_action.

  • ROSPublishingHAL(*, node, description, skill_id_getter=..., skill_revision_getter=..., joint_state_topic="/joint_states", candidate_action_topic="/openral/candidate_action") (L110) — Implements the structural openral_hal.protocol.HAL Protocol. connect opens a publisher on candidate_action_topic and subscribes to joint_state_topic; read_state returns the latest cached JointState; send_action serialises the Action Pydantic into openral_msgs/ActionChunk (row-major flat, trace_id from current OTel context, rskill_id / rskill_revision from the injected getters) and publishes. estop raises ROSEStopRequested.
  • _row_major_flatten(rows) -> list[float] (L47) — Private helper used by _action_to_chunk; preserves row-major ordering for the chunk flat array.
  • _CONTROL_MODE_TO_UINT8: dict[ControlMode, int] (L107) — Stable mapping from openral_core.ControlMode to the uint8 slot in ActionChunk.control_mode.

python/runner/src/openral_runner/slam_bridge.py

ADR-0025 — rclpy → OTLP bridge for slam_toolbox /map. Throttles to 1 Hz, rasterises the nav_msgs/OccupancyGrid to a base64 PNG, looks up the robot's map-frame pose via tf2, and emits one slam.occupancy_grid OTel span the dashboard's SLAM Map card renders (store handler in openral_observability.dashboard.store).

  • SLAM_MAP_TOPIC_DEFAULT = "/map" (L48) — Default nav_msgs/OccupancyGrid topic slam_toolbox publishes on.
  • encode_occupancy_grid_png(*, width: int, height: int, data: list[int]) -> str (L75) — Pure function rendering an OccupancyGrid.data array as a base64 PNG (unknown→mid-grey, free→white, occupied→black, in-between linear ramp; flipped so map-north points up). Raises ValueError if len(data) != width * height. Exercised directly by tests against synthetic grids.
  • yaw_from_quaternion_xyzw(x: float, y: float, z: float, w: float) -> float (L133) — Planar yaw (rad, CCW about +Z) from a quaternion (x, y, z, w); ZYX extraction reduced to the yaw term (roll/pitch ignored for the planar base). Returns yaw in [-pi, pi].
  • robot_pose_from_transform(*, translation_xyz: tuple[float, float, float], rotation_xyzw: tuple[float, float, float, float]) -> tuple[float, float, float] (L157) — Planar (x, y, yaw) from a tf2 transform's translation + rotation (z ignored); delegates yaw to yaw_from_quaternion_xyzw. Used by SlamMapBridge to project the map→base_frame lookup into the span attributes.
  • class SlamMapBridge (L188) — rclpy.node.Node-hosted subscription on /map; on each accepted callback rasterises the grid, looks up the robot pose, and emits a slam.occupancy_grid span (openral.slam.frame_id/width/height/resolution_m/origin_x/origin_y/png_b64/source_node, plus robot_x/robot_y/robot_yaw/base_frame when the tf2 lookup succeeds and footprint_radius_m when known). Degrades gracefully (no robot marker) when TF is unavailable.
  • __init__(node, *, topic=SLAM_MAP_TOPIC_DEFAULT, base_frame="base_link", footprint_radius_m=None, footprint_polygon=None, source_node_name="openral_slam_toolbox", publish_interval_s=1.0, max_cells=4_000_000) (L234) — Subscribes to topic with slam_toolbox's RELIABLE + TRANSIENT_LOCAL + KEEP_LAST=1 QoS and prepares the OTel tracer. base_frame is the tf2 child frame whose map-frame pose is emitted; footprint_radius_m (when set) is emitted so the dashboard can draw the footprint circle; footprint_polygon (list[tuple[float, float]] | None, base-frame XY metres) is flattened to [x0,y0,x1,y1,…] and emitted as openral.slam.footprint_polygon_xy so the dashboard can draw the true oriented base outline (falling back to the footprint_radius_m circle).
  • destroy() -> None (L301) — Release the ROS subscription. Safe to call multiple times.

packages/openral_foxglove_bringup/ (ADR-0059)

Read-only Foxglove live-scene surface (hybrid with the OTel dashboard). Launches: foxglove.launch.py (the read-only bridge + opt-in compressed-image republishers), bucket2.launch.py (the converter node), record.launch.py (MCAP recorder). Spawned into deploy-sim by openral deploy sim --foxglove.

  • BUCKET1_TOPIC_WHITELIST: list[str] (topics.py L14) — Explicit foxglove_bridge topic_whitelist (camera images + /compressed siblings, /map, octomap cloud, joints, TF, robot_description, and the Bucket-2 converter outputs). Anything unlisted — notably the safety/e-stop/action topics — is never exposed. Imported by both foxglove.launch.py and sim_e2e.launch.py so the allowlist has one source of truth.
  • READ_ONLY_CAPABILITIES: list[str] (topics.py L42) — ["connectionGraph", "assets"]; omits clientPublish/services/parameters so a connected viewer cannot publish, call services, or write params (cannot actuate).
  • class MarkerSpec (bucket2_markers.py L58) — Frozen dataclass: one visualization_msgs/Marker's pose/scale/type as plain data (ROS-free, so the conversion is unit-testable).
  • capsule_markers(radius, half_length, origin_xyzrpy, object_id) -> list[MarkerSpec] (bucket2_markers.py L100) — Pure: convert openral_msgs/WorldCollision parallel capsule arrays to cylinder marker specs (length 2·half_length, scale = diameter 2·radius; sphere → zero-length cylinder). Raises ValueError on array-length mismatch.
  • occupied_voxel_centers(origin, resolution, size, occupancy) -> list[tuple[float,float,float]] (bucket2_markers.py L171) — Pure: centre coordinates of occupied voxels from openral_msgs/OccupancyVoxels (row-major idx = x + size_x*(y + size_y*z), +0.5 cell-centre offset). Raises ValueError when len(occupancy) != size_x*size_y*size_z.
  • class Bucket2MarkersNode (bucket2_markers.py L223) — rclpy.node.Node subscribing /openral/world_collisions + /openral/world_voxels; re-publishes /openral/world_collisions_markers (MarkerArray) + /openral/world_voxels_cloud (PointCloud2) via the pure functions. Read-only viz; defers rclpy/openral_msgs imports.
  • main() -> None (bucket2_markers.py L372) — Console entry point (installed as lib/openral_foxglove_bringup/bucket2_markers); rclpy.init → spin → shutdown.