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. 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. 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. Subscribes /joint_states, /openral/policy_state, camera/perception streams; publishes typed fast/slow world-state summaries. Params include staleness_limit_s (0.5) and policy_state_staleness_limit_s (5.0 — the step-locked policy_state component's own window). The composed runtime shares the same aggregator with rskill_runner_node, so simulator-native policy state reaches the active rSkill without an untyped side channel.
  • _direct_image_frame_sensors() -> set[str] — the direct_image_frame_sensors parameter as a set. _on_image returns immediately for these cameras: the co-located sensor leg's _AggregatorPump already writes them to the aggregator (zero-copy handles intact) and emits their dashboard span at full reader cadence, so re-doing either here would only add a slower duplicate off the rate-capped tee.
  • 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 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) — 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, 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_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: 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 (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. LifecycleRosImageObjectDetectorNode(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 "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) -> NoneScene-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: 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. Launched by openral deploy sim --enable-scene-vlm (enable_scene_vlm:=true → the launch brings this node up with one cameras entry per manifest RGB camera and sets the reasoner's scene_query_available:=true). Until that wiring landed nothing set the param and no launch file started this node, so query_scene was never offered — and the file was committed non-executable (mode 100644), which install(PROGRAMS ...) preserves, so executable="scene_vlm_node.py" could not resolve in the libexec dir even once launched.
  • packages/openral_perception_ros/openral_perception_ros/reward_monitor_node.pymain(args=None) -> NoneReward-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 reward backend from a kind:"reward" manifest_path (_build_monitorbuild_reward_monitor; Robometer defaults to RobometerInProcessReward, TOPReward to TOPRewardMonitor). query_task_progress service: serves /openral/perception/query_task_progress (openral_msgs/srv/QueryTaskProgress) — read-only windowed progress/success assessment (_on_query_task_progress → backend .assess); returns ok=False, stale=True when no fresh frame / no task. Every successful assessment (the service query and the scoring heartbeat below) emits a reward.score OTel span (_emit_score_spansemconv.SPAN_REWARD_SCORE, attrs reward.{progress,success,stalled,succeeded,frames,task,camera}) so the dashboard's rSkill card renders a live colour-banded progress/success bar plus which camera the monitor is attending to (reward.camera, e.g. "top" — module-level _camera_label(topic) -> str extracts the <name> from the conventional /openral/cameras/<name>/image topic, computed once into self._camera_name from the primary camera's topic since primary_camera is usually the uninformative single-camera-fallback id "default"); main() calls configure_observability(service_name="openral.reward_monitor") so the spans flow when the node runs standalone. The reward counterpart of scene_vlm_node; the rolling buffer lives node-side and reward backends score on demand. Params: cameras, primary_camera, image_topic, manifest_path (required), task, score_period_s, score_window_s. The reasoner offers query_task_progress when task_progress_available is set. Advisory-only — no actuation. Scoring heartbeat (_score_tick): a score_period_s timer (default 2 s) scores a bounded RECENT window (score_window_s, default 2 s — kept small so the reward forward fits beside a VLA on an 8 GB card; the full 40 s buffer subsampled to max_frames=8 OOMs) with the executing instruction and emits the reward.score span that drives the dashboard bar. Created when execution-gating bounds it (gate_scoring_on_execution, the deploy default) or the critic leg is on; it self-limits to _vla_active (set from /openral/reward/active_task, which the rskill runner publishes around each execute_rskill and the reasoner mirrors in a mission-driven deploy), so it never scores an idle scene. Optional CriticScore publish (enable_critic_score): when on, each heartbeat also publishes a generic openral_msgs/CriticScore (critic_id=manifest name, score=progress_now via critic_score_from_assessment, threshold=critic_score_threshold default 0.8, trace_id from the active span) on critic_score_topic (default /openral/critic/score) — the Tier-C critic_producer_node source. enable_critic_score gates ONLY this publish; the scoring + dashboard bar run regardless. The deploy launch sets enable_critic_score:=enable_critic, so --enable-reward-monitor --enable-critic makes the reward monitor drive /openral/failure/critic. Best-effort: skips silently when gated off / stale / no-task / empty-buffer; never crashes the timer. destroy_node() calls backend .close() before tearing down so in-process VLMs release CUDA memory.
  • 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_perception_ros/openral_perception_ros/depth_convert.pyMetric-depth message boundary (no torch). depth_array_to_image_msg(depth_m, *, frame_id, stamp=None) builds a 32FC1 sensor_msgs/Image in metres (NaN = no return) with tightly-packed rows; image_msg_to_depth_array(msg) is its inverse; camera_info_from_intrinsics(*, fx, fy, cx, cy, width, height, frame_id, stamp=None) builds a plain pinhole CameraInfo (plumb_bob, zero distortion, K/P matrices). Raises DepthConvertError on non-2D depth, a non-32FC1 encoding, or a padded stride (step != width*4). Used by depth_provider_node to feed nvblox.
  • packages/openral_perception_ros/openral_perception_ros/depth_provider_node.pymain(args=None) -> NoneMonocular metric-depth provider. Defines DepthProviderNode(Node) lazily inside main(): subscribes a mono RGB sensor_msgs/Image (image_topic), forwards each frame (PNG over ZMQ REQ + msgpack) to the DA3 depth sidecar (tools/da3_depth_sidecar.py / tools/_da3_depth_server.py, default depth-anything/DA3-SMALL — measured 0.27 GB / ~27 Hz on an 8 GB Ada), and republishes the returned metric depth as a 32FC1 Image (depth_topic) + CameraInfo (camera_info_topic) via depth_convert, stamped in depth_frame_id. These are the topics nvblox.launch.py remaps onto nvblox's depth/image + depth/camera_info, giving lidar-less robots a Nav2 cost map (cuVSLAM pose + nvblox). Best-effort: an unconvertible frame or sidecar hiccup is logged at warning and skipped, never crashing the graph. Params: image_topic, depth_topic, camera_info_topic, depth_frame_id, sidecar_host, sidecar_port, process_res, request_timeout_ms. A failed request also rebuilds the REQ socket (_make_socket) — a REQ socket refuses every send after a timed-out recv (EFSM), so one slow reply (e.g. the autostarted sidecar still provisioning its venv) would otherwise wedge depth forever. The mono visual-SLAM launch autostarts the sidecar (slam_depth_sidecar_autostart); the pure depth_convert half is unit-tested.
  • packages/openral_slam_bringup/openral_slam_bringup/depth_height_filter_node.pymain(args=None) -> NoneNvblox floor-exclusion prefilter. Defines DepthHeightFilterNode(Node) lazily inside main(): subscribes a 32FC1 depth image + CameraInfo, derives a robot-relative navigation-height band from robot_yaml (RobotDescription footprint, collision geometry, and link transforms), shifts it by the live global_frame -> base_frame TF, looks up the depth optical frame in global_frame, and republishes a filtered depth image where pixels whose back-projected global z is outside that band are zeroed before nvblox integrates them. This is required for mapping_type: static_occupancy: Isaac ROS nvblox applies static_mapper.workspace_bounds_* to TSDF view calculation but its camera occupancy integrator still projects raw floor returns into the 2D /map; the prefilter makes /map a floor-excluded obstacle grid without a per-scene hardcoded map-z band. Pure helpers: RobotRelativeHeightBand, derive_robot_relative_height_band(description, *, floor_clearance_m=0.10, min_body_height_m=0.30) -> RobotRelativeHeightBand, quaternion_to_matrix_z_row(x, y, z, w) -> tuple[float, float, float], and filter_depth_by_global_height(depth_m, *, fx, fy, cx, cy, rotation_z_row, translation_z_m, min_height_m, max_height_m) zero out-of-band/invalid pixels and raise ValueError on invalid intrinsics, shape, or band. Params: input_depth_topic, input_camera_info_topic, output_depth_topic, output_camera_info_topic, global_frame, base_frame, robot_yaml, floor_clearance_m, min_body_height_m, optional paired overrides min_height_m/max_height_m, and tf_timeout_ms. Launched by nvblox.launch.py before the NVIDIA component.
  • packages/openral_slam_bringup/openral_slam_bringup/pycuvslam_node.pymain(args=None) -> NoneIn-process cuVSLAM visual SLAM (PyCuVSLAM wheel), stereo or mono RGBD. Defines PyCuVSLAMNode(Node) lazily inside main(): synchronizes a rectified stereo pair — or, when depth_image_topic is set, one RGB camera + a metric-depth stream (mono RGBD, _build_mono_tracker/_on_rgbd: single-camera rig ≡ the RGB optical frame, OdometryMode.RGBD with depth_camera_id=0, depth encoded by depth_to_uint16_mm at depth_scale_factor; the DA3 depth provider is the producer — the one-camera path for lidar-less, stereo-less robots) — from the OpenRAL camera bus (message_filters.ApproximateTimeSynchronizer, sync_slop_s), builds a cuvslam.Rig from the CameraInfo(s), tracks with cuvslam.Tracker (enable_slam toggles loop-closure SLAM vs pure VO; stereo only), publishes nav_msgs/Odometry (odometry_topic, map←rig), and broadcasts the same map → odom TF edge the other SLAM backends fill (_emit) by composing the tracked pose with the live odom ← rig TF at the image stamp. Two stereo rig modes (_build_tracker): (a) multi-camera — when rig_frame is set, or derived from robot_yaml's base_frame — each camera's rig_from_camera extrinsic is read straight from TF (transform_to_pose), which is cuVSLAM's default mode (odometry_mode=Multicamera, rectified_stereo_camera=False) and handles arbitrary, e.g. toed-in, base-mounted sim rigs (the rig ≡ that frame); (b) rectified baseline — when neither is set, rig ≡ left camera optical frame and the right camera sits at a pure x-baseline read from its P matrix (stereo_baseline_m), for a standalone RealSense-style rectified pair. The alternative to the composable isaac_ros_visual_slam path (cuvslam.launch.py) for hosts without the Isaac ROS apt stack — same NVIDIA engine from the operator-installed PyCuVSLAM wheel (never bundled; missing import raises ROSConfigError at startup). Pure helpers: stereo_baseline_m(right_p) -> float (raises ValueError unless the rectified right P encodes a positive baseline), transform_to_pose(transform) (a geometry_msgs/Transformrig_from_camera pose), compose_pose(a, b) / invert_pose(p) / map_from_odom(map_from_rig, odom_from_rig) over ((qx,qy,qz,qw),(tx,ty,tz)) poses, and depth_to_uint16_mm(msg, width, height, scale) (a 32FC1 metres Image → the uint16 grid cuVSLAM RGBD eats: PIL float-bilinear resize onto the RGB resolution, metres × scale clipped to uint16 — the inverse of cuVSLAM's depth_scale_factor divide; raises ValueError on a non-32FC1 encoding). Params: left/right_image_topic, left/right_camera_info_topic, map_frame, odom_frame, odometry_topic, sync_slop_s, tf_timeout_ms, enable_slam, rig_frame, robot_yaml, depth_image_topic (non-empty selects mono RGBD), depth_scale_factor (default 1000 = millimetres). Launched standalone via pycuvslam.launch.py; deploy-wired for panda_mobile_vslam in scenes/deploy/robocasa_vslam.yaml (stereo) and scenes/deploy/robocasa_vslam_mono.yaml (mono RGBD). The real-engine tracking test is GPU-gated (test_pycuvslam_node.py).
  • packages/openral_safety/openral_safety/supervisor_node.pySafetyPassthroughNode(node_name="openral_safety") (L57), main(args=None) (L341) — Day-1 pass-through. Lifecycle node owning /openral/candidate_action → /openral/safe_action plus /openral/estop + /openral/estop_reset (std_srvs/Trigger). Per-mode envelope checks include joint bounds, Cartesian delta/twist, body twist, and gripper ranges. CARTESIAN_DELTA applies the same optional clip(raw,-1,1) * cartesian_delta_scale physical conversion as the C++ predictive kernel, so the two safety layers cannot disagree on normalized controller units. SafetySupervisorNode is a back-compat alias.

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 and the /openral/estop latch 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) — 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) (branches on manifest.kind to route VLAs to the local/HF-Hub path and ros_action/ros_service skills to ROSActionRskill). The local policy shim derives gripper slots from RobotDescription.joints[].role when a checkpoint exposes only action shape and applies optional policy_extras.gripper_scale in both directions; this maps lerobot SO-101 [0,100] to the HAL's normalized [0,1] without treating the channel as degrees. The execute loop catches ROSRskillGoalSatisfied specifically and closes the goal with success=True. _run_until_done_or_deadline(...) -> str returns why it exited ("completed" / "deadline" / "cancelled") — every exit used to be a bare return, so a goal that blew its execution budget was reported success=True (observed live: a 144.5 s first inference against a resolved 45 s budget still closed SUCCEEDED). A "deadline" exit now aborts with failure_reason="deadline_exceeded: elapsed=…s budget=…s" so the reasoner's replanning ladder can act (CLAUDE.md §3). Its 30 Hz pacing sleeps to an ABSOLUTE per-tick deadline via _pace_tick (module-level, over openral_runner.clock.sleep_until), so inference/publish work comes out of the period rather than being added to it, and an overrun re-anchors instead of bursting to catch up. The former unconditional sleep(period_s) added 33.3 ms after every tick and reduced a measured rollout to 14.6 Hz before chunk stalls; the interim _remaining_tick_sleep_s(period, elapsed) fixed the double-count but re-based the period after each sleep, so scheduler jitter accumulated as drift. _deadline_lapsed(start, budget_s, chunks) -> bool owns the check, emits openral.event.deadline_missed, and records the true elapsed (the budget is only testable between steps, so a blocking skill.step() overruns it by up to one step). Publishes the executing instruction on /openral/reward/active_task (std_msgs/String, _publish_active_task) when a goal starts and "" on every exit via _reset_active_goal, so the reward monitor's scoring heartbeat gates on real execution even with no reasoner in the loop (a direct execute_rskill dispatch); advisory-only, fully suppressed on failure.
  • 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).
  • 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, enable_world_cloud_bridge=False, world_cloud_topic="", slam_source_node="", dataset_out=None, …) -> ComposedRuntime (L69) loads any in-tree robots/<id>/robot.yaml via RobotDescription.from_yaml, builds a single WorldStateAggregator, hands the same reference to a colocated _WorldStateLifecycleNode and RskillRunnerNode, and always attaches SlamMapBridge so any existing /map OccupancyGrid publisher can populate the dashboard independently of the SLAM backend launch flag (slam_source_node overrides the card's openral_slam_toolbox source label — the visual backend sets openral_nvblox). world_cloud_topic re-points the opt-in WorldCloudBridge from octomap's latched centers to nvblox's VOLATILE ESDF cloud (latched=False). Honors the "only subscriber of /joint_states" rule. 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/run 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). The dashboard SLAM Map bridge is now always part of compose_runtime; there is no enable_slam_bridge launch parameter. Params world_cloud_topic + slam_source_node (both default "") thread the mono visual-SLAM dashboard wiring through to compose_runtime (nvblox voxel-cloud topic + SLAM-card source label). Real deploys (openral deploy run): the deploy_config parameter carries the DeployScene YAML path; when set the script opens every deploy-bound SensorSpec (robot manifest ∪ scene sensors:) via open_deploy_sensor_readers before spinning and closes them in the teardown finally. Script top eagerly runs gi + Gst.init() BEFORE any numpy/pydantic/rclpy import — under Fast-DDS a later rclpy.Node() segfaults when numpy/pydantic loaded first (x86 Ubuntu 24.04 / system PyGObject; bisected 2026-07-02).
  • _prewarm_vla_imports() -> None (L49) — Imports torch + lerobot.policies.factory (the shared ~95% of any VLA adapter's import phase) inside phase_timer("vla_framework", prefix="prewarm"), emitting prewarm_vla_framework_{start,heartbeat,done}. except ImportError: pass so slim installs skip cleanly. Second load-bearing ordering constraint in this file, alongside the gi/Gst.init() one above: the call must stay AHEAD of open_deploy_sensor_readers. transformers' import runs importlib.metadata.packages_distributions() — a stat() per file of every installed distribution — and each stat releases the GIL for a 30 fps reader thread to snatch. Measured on the SO-101 bench: 23 s in a quiet process, 8+ minutes (never finished) with two readers up. The convoy is on GIL re-acquisition after a syscall, which is why neither UV_COMPILE_BYTECODE=1 nor phase_timer's raised switch interval rescued it — pre-warming before any reader exists is the only fix that works. Runs on the sim path too (it is not gated on deploy_config): sim spawns no in-process readers and so has no convoy, but the ~7 s import then overlaps the HAL's concurrent on_configure (~6 s MuJoCo arm / ~27 s cold robocasa) instead of being charged to the first ExecuteRskill goal.
  • packages/openral_rskill_ros/launch/sim_e2e.launch.py — Generic deploy ROS graph. Reasoner launch args are model-first: reasoner_model (default OPENRAL_REASONER_MODEL or curated gpt-5.5) and optional reasoner_endpoint; these become OPENRAL_REASONER_{MODEL,ENDPOINT,MAX_TOKENS} on the reasoner node. reasoner_endpoint takes a named endpoint or a URL. The deprecated reasoner_provider arg was removed in 0.3.0 with the OPENRAL_REASONER_LLM_* shim. Launch arg workcell_json carries the DeployScene safety/ACM subset when non-empty; the launch parses it, calls compute_intersection(robot, skill=None, deploy=...), applies merge_extra_allowed_pairs, and forwards the resulting kernel params. Visual-SLAM args: slam_visual_impl, slam_stereo_cameras, slam_mono_camera (non-empty → _build_visual_slam_includes composes the mono RGBD leg: pycuvslam in RGBD mode + the DA3 depth provider (framed at the camera's manifest frame_id) + nvblox — always, not gated on nav2), and slam_depth_sidecar_autostart (default true → an ExecuteProcess runs tools/da3_depth_sidecar.py --port 5771; a port-conflicting operator sidecar wins because the child's death never tears down the launch).
  • packages/openral_rskill_ros/openral_rskill_ros/sensor_leg.py — Real-mode camera leg for openral deploy run (the real-hardware counterpart of the sim HAL's SimSensorBridge).
  • open_deploy_sensor_readers(sensors, *, topic_prefix="/openral/cameras", aggregator=None, ros_node=None, uncapped_sensors=(), topic_max_size=None) -> SensorLeg (L113) — Builds the complete deploy-bound batch through make_sensor_readers, so multi-view backends can share explicitly scoped resources (the A1 front/wrist readers share one paired session) without module-global state. Specs without a deploy_binding are skipped. Every camera publishes on <topic_prefix>/<name>/image: gstreamer backends get the in-pipeline ROS tee forced on (publish_to_ros=True); other backends are wrapped in a polling SensorRosPublisher, with manifest frame/intrinsics and companion CameraInfo. When the composed runtime's shared aggregator is passed, every reader also gets an _AggregatorPump writing frames directly into WorldStateAggregator.update_image_frame; half-open legs are closed before errors propagate.
  • _AggregatorPump — daemon-thread pump (start/stop like SensorRosPublisher) polling read_latest at the spec rate into the aggregator; deduplicates on the frame's monotonic stamp so a re-polled latched frame never refreshes the staleness stamp. After each aggregator write it calls _emit_frame_observability, guarded so a failing display path can never starve the policy.
  • _emit_frame_observability(sensor_name, frame, flip_180) -> None — emits the dashboard's sensors.read_latest span (modality/encoding/geometry/age + JPEG thumbnail) for a pump-fed camera, honouring OPENRAL_DASHBOARD_FLIP_180 on a display copy only. Exists because _fallback_topic_rate_hz caps the ROS tee that WorldState's _on_image rides, which would leave the camera tiles at 3 fps; measured 2.42 ms/frame at 320×240 q60, and 60 thumbnails/s costs 4.5 % of a competing thread's GIL against the 89.5 % the uncapped image topic held.
  • _publish_rate_hz(spec) -> float — binding fps, else spec rate_hz, else 10 Hz. This is the capture/native-tee cadence and is not capped.
  • _fallback_topic_rate_hz(spec) -> floatmin(_publish_rate_hz(spec), _MAX_FALLBACK_TOPIC_RATE_HZ) (3 Hz — re-profiling after the first 5 Hz cap still put 52.75 % of GIL samples in _publish_frame; the floor is DEFAULT_STALENESS_S 0.5 s, so 2 Hz would flap the diagnostics). Applies only to the Python SensorRosPublisher fallback, whose every tick is a GIL-held rclpy Python→C conversion of a full-resolution Image — profiled at 89.5 % of all GIL-holding samples during a VLA load. Readers still capture at full fps and the policy reads the freshest frame in-process, so only the ROS topic cadence drops; the native GStreamer tee is deliberately uncapped.
  • topic_frame_size(runtime) -> tuple[int, int] | None — resolution ceiling for the fallback topic, derived from the scene's DeployRuntime at launch (_DEFAULT_TOPIC_MAX_SIZE = 320x240). Deliberately not per-rSkill: the reasoner picks skills at runtime, and the topic's subscribers are fixed by launch flags, not by which policy is loaded. Returns None (native pixels) when the object detector is on (its node declares input_size 640) or SLAM is on (cuVSLAM triangulates against calibrated intrinsics). The policy is unaffected either way — it reads the aggregator in-process at capture resolution, which is what ACT (no resize, 640x480 exact) and SmolVLA (its own 512x512 pad-resize) consume.
  • apply_launch_overrides(runtime, *, enable_object_detector=None, enable_slam=None, slam_stereo_cameras=None, slam_mono_camera=None) -> object | None — folds the launch's RESOLVED consumer flags over the scene's raw runtime block before slam_camera_names / topic_frame_size consume it. The scene's enable_object_detector / enable_slam are tri-state (None = "auto") and only the deploy CLI resolves the auto — the runtime node re-reads the original YAML, so trusting the raw block would rate-cap + downscale the very cameras an auto-enabled detector/SLAM leg subscribes to. None overrides keep the scene's value; no runtime block AND no overrides returns None (the callers' conservative native-pixels contract). Fed from the resolved_* bootstrap parameters sim_e2e.launch.py passes to runtime_node.
  • merge_deploy_sensors(manifest_sensors, scene_sensors) -> list[SensorSpec] — Robot-manifest ∪ scene sensors with the scene entry winning on name collision (a same-named scene entry is that robot sensor's deploy binding — keeping both would double-open the device).
  • SensorLeg (L62) — dataclass holding readers + publishers + direct_sensors (forward to WorldState's direct_image_frame_sensors parameter; _on_image then skips those sensors entirely, because the pump already owns both their aggregator write and their dashboard span); close() (L78) stops publishers first, then closes readers, idempotently and exception-safe (teardown must always reach the HAL shutdown behind it).

python/runner/src/openral_runner/ros_publishing_hal.py

HAL Protocol adapter that publishes ActionChunk on /openral/candidate_action.

  • ROSPublishingHAL(*, node, description, skill_id_getter=..., skill_revision_getter=..., tick_index_getter=lambda: 0, joint_state_topic="/joint_states", candidate_action_topic="/openral/candidate_action", action_applied_topic="/openral/action_applied") (L109) — Publishes typed candidate ActionChunks and applies bounded backpressure after the final slot of an atomic tick until the HAL acknowledges application. The acknowledgement carries only the monotonic tick id; deploy sim has no simulator-oracle completion or reset channel. estop raises ROSEStopRequested.
  • _row_major_flatten(rows) -> list[float] (L49) — Private helper used by _action_to_chunk; preserves row-major ordering for the chunk flat array.
  • _CONTROL_MODE_TO_UINT8: dict[ControlMode, int] (L106) — Stable mapping from openral_core.ControlMode to the uint8 slot in ActionChunk.control_mode.

python/runner/src/openral_runner/slam_bridge.py

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" (L43) — Default nav_msgs/OccupancyGrid topic slam_toolbox publishes on.
  • encode_occupancy_grid_png(*, width: int, height: int, data: list[int]) -> str (L70) — 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.
  • robot_pose_from_transform(*, translation_xyz: tuple[float, float, float], rotation_xyzw: tuple[float, float, float, float]) -> tuple[float, float, float] (L128) — Planar (x, y, yaw) from a tf2 transform's translation + rotation (z ignored); delegates yaw to openral_core.geometry.quat_xyzw_to_yaw. Used by SlamMapBridge to project the map→base_frame lookup into the span attributes.
  • class SlamMapBridge (L159) — 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) (L205) — 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 (L281) — Release the ROS subscription. Safe to call multiple times.

packages/openral_foxglove_bringup/

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.

packages/openral_reasoner_ros/openral_reasoner_ros/critic_producer_node.py

Tier-C critic producer — default publisher for the reserved /openral/failure/critic source (observability audit P1 R3). Subscribes the generic /openral/critic/score topic (openral_msgs/CriticScore) that any reward model publishes (Robometer, future SARM, success classifiers), routes each (critic_id, score, threshold) sample through openral_reasoner.CriticWatchdogGroup, and on a stall emits a FailureTrigger (KIND_CRITIC / SEVERITY_FAIL, the score's trace_id propagated) via openral_observability.FailureBusPublisher; reasoner_node maps it to a forced Tier-C tick. Advisory only (CLAUDE.md §1.1). Exported from openral_reasoner_ros.

  • class CriticProducerNode (L60) — rclpy.node.Node. __init__() reads params score_topic (default /openral/critic/score), stall_patience (default 5), min_delta (default 0.02 — a small floor so sub-threshold reward-model noise does not re-arm a latched watchdog); builds a CriticWatchdogGroup + a FailureBusPublisher(FailureSource.CRITIC); subscribes CriticScore (RELIABLE+VOLATILE+KL=10). _on_score(msg) routes the sample and publishes a KIND_CRITIC/SEVERITY_FAIL FailureTrigger on a stall (watchdog latches → one event per stall). destroy_node() tears down the bus publisher first.
  • main(args=None) -> None (L126) — Console entry point (installed as lib/openral_reasoner_ros/critic_producer_node.py); rclpy.init → spin → shutdown.