ROS 2 Lifecycle Nodes (packages/)
Part of the OpenRAL public-symbol inventory. Hand-curated;
(LNN)markers are refreshed bytools/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.py—main() -> None(L46) — SO-100 LeRobot HAL. Heartbeat wired.packages/openral_hal_openarm/openral_hal_openarm/lifecycle_node.py—main() -> None(L57) — Enactic OpenArm v2 bimanual HAL. Wrapsopenral_hal.OpenArmMujocoHAL(16-DoF). Subscribes to/openral/safe_action+/openral/estop; publishes/joint_states. Heartbeat wired. Lifecycle integration coverage intests/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.py—main() -> None—WorldStateAggregatorlifecycle node. Subscribes/joint_states,/openral/policy_state, camera/perception streams; publishes typed fast/slow world-state summaries. Params includestaleness_limit_s(0.5) andpolicy_state_staleness_limit_s(5.0 — the step-locked policy_state component's own window). The composed runtime shares the same aggregator withrskill_runner_node, so simulator-native policy state reaches the active rSkill without an untyped side channel._direct_image_frame_sensors() -> set[str]— thedirect_image_frame_sensorsparameter as a set._on_imagereturns immediately for these cameras: the co-located sensor leg's_AggregatorPumpalready 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 ofbuild_world_state_stamped_msg: reconstructs anopenral_core.WorldStatefrom aWorldStateStampedmsg (joint/base/EE/diagnostic state plus thedetected_object_*parallel arrays →list[DetectedObject]). Used by the reasoner_node to feed real state into itsContextRenderer.image_framesis alwaysNone(the IDL carries image topic refs, not inline pixels).packages/openral_perception_ros/openral_perception_ros/ros_image_detector_node.py—main(args=None) -> None(L51) — Standalone ROS-Image object detector (no GStreamer). Defines the node class lazily insidemain()behind ROS imports:RosImageObjectDetectorNode(LifecycleNode)— subscribes a camerasensor_msgs/Imageonimage_topic, runs anopenral_runnerdetector backend, and publishesopenral_msgs/PromptStamped(carrying the detector'sObjectsMetadataasmetadata_json) onoutput_topic(default/openral/perception/objects),header.frame_idstamped withsensor_id. Backend selection (_build_detector, 2026-06-09 amendment): with amanifest_pathparam set, builds viaopenral_runner...detector_factory.build_manifest_detector(RT-DETR ONNX forruntime: onnx, or the open-vocabLocateAnythingDetector/VLM_SIDECARforruntime: pytorch); without it, the legacyObjectsDetector(RT-DETR ONNX) fromonnx_path+labels. Detector mode —_resolve_wiringreads the manifest'sdetector.modeaton_configure(viadetector_node_wiring): acontinuousdetector runs the primary camera's detect+publish leg and does NOT exposelocate_in_view/ subscribedetector_query; anon_demanddetector 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 initialqueryparam is applied and the node subscribes astd_msgs/Stringquery_topic(default/openral/perception/detector_query) to retarget the persistent query live. Camera-agnostic: acamerasparam maps logical ids → image topics (falling back to a singleimage_topicunderprimary_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, usesdetect_with_queryso the persistent query is untouched); offered only foron_demanddetectors 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 —RosImageObjectDetectorNode(LifecycleNode): the GPU detector backend is built onon_activateand released onon_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_publishnever 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)— adetect()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 →infoliveness 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 theDETECTOR_LOG_LEVEL_ENV(OPENRAL_DETECTOR_LOG_LEVEL) env var vianormalize_log_level(value) -> str | None(L83, pure: case-insensitive,warning→WARN, unknown→None) +rclpy.logging.set_logger_level, soOPENRAL_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 viaopenral deploy sim --enable-object-detector(--object-detector-manifestselects the VLM); the reasoner offerslocate_in_viewwhendetector_availableis set.packages/openral_perception_ros/openral_perception_ros/scene_vlm_node.py—main(args=None) -> None— Scene-VLM query service node. DefinesSceneVlmNode(Node)lazily insidemain()behind ROS imports: subscribes one or more camerasensor_msgs/Imagestreams, caches each camera's latest BGR frame (_make_cache_cb), and builds aQwenSceneVlmbackend from akind:"vlm"manifest_path(_build_vlm→build_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_scene→QwenSceneVlm.query, returns free text); offered only if the IDL is built. The scene-reasoning counterpart ofros_image_detector_node(which serveslocate_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 offersquery_scenewhenscene_query_availableis set. Launched byopenral deploy sim --enable-scene-vlm(enable_scene_vlm:=true→ the launch brings this node up with onecamerasentry per manifest RGB camera and sets the reasoner'sscene_query_available:=true). Until that wiring landed nothing set the param and no launch file started this node, soquery_scenewas never offered — and the file was committed non-executable (mode 100644), whichinstall(PROGRAMS ...)preserves, soexecutable="scene_vlm_node.py"could not resolve in the libexec dir even once launched.packages/openral_perception_ros/openral_perception_ros/reward_monitor_node.py—main(args=None) -> None— Reward-monitor query service node. DefinesRewardMonitorNode(Node)lazily insidemain(): subscribes the co-active VLA's camerasensor_msgs/Imagetopic(s), buffers frames per camera into aRollingFrameBuffer(downsampled to the manifest'starget_fps, stamped on the node clock so eviction/staleness work in sim + real), and builds a reward backend from akind:"reward"manifest_path(_build_monitor→build_reward_monitor; Robometer defaults toRobometerInProcessReward, TOPReward toTOPRewardMonitor). 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); returnsok=False, stale=Truewhen no fresh frame / no task. Every successful assessment (the service query and the scoring heartbeat below) emits areward.scoreOTel span (_emit_score_span→semconv.SPAN_REWARD_SCORE, attrsreward.{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) -> strextracts the<name>from the conventional/openral/cameras/<name>/imagetopic, computed once intoself._camera_namefrom the primary camera's topic sinceprimary_camerais usually the uninformative single-camera-fallback id"default");main()callsconfigure_observability(service_name="openral.reward_monitor")so the spans flow when the node runs standalone. The reward counterpart ofscene_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 offersquery_task_progresswhentask_progress_availableis set. Advisory-only — no actuation. Scoring heartbeat (_score_tick): ascore_period_stimer (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 tomax_frames=8OOMs) with the executing instruction and emits thereward.scorespan 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 eachexecute_rskilland 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 genericopenral_msgs/CriticScore(critic_id=manifest name,score=progress_nowviacritic_score_from_assessment,threshold=critic_score_thresholddefault 0.8,trace_idfrom the active span) oncritic_score_topic(default/openral/critic/score) — the Tier-Ccritic_producer_nodesource.enable_critic_scoregates ONLY this publish; the scoring + dashboard bar run regardless. The deploy launch setsenable_critic_score:=enable_critic, so--enable-reward-monitor --enable-criticmakes 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.py—image_to_bgr_bytes(msg) -> tuple[bytes, int, int](L14) — Converts asensor_msgs/Image(rgb8/bgr8, tightly-packed rows) to contiguous H·W·3 BGRuint8bytes plus(width, height)forObjectsDetector.detect;rgb8is reversed to BGR,bgr8passes through. RaisesImageConvertError(L10) on an unsupported encoding or a padded row stride (step != width*3). Nocv_bridgedependency.packages/openral_perception_ros/openral_perception_ros/depth_convert.py— Metric-depth message boundary (no torch).depth_array_to_image_msg(depth_m, *, frame_id, stamp=None)builds a32FC1sensor_msgs/Imagein 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 pinholeCameraInfo(plumb_bob, zero distortion, K/P matrices). RaisesDepthConvertErroron non-2D depth, a non-32FC1encoding, or a padded stride (step != width*4). Used bydepth_provider_nodeto feed nvblox.packages/openral_perception_ros/openral_perception_ros/depth_provider_node.py—main(args=None) -> None— Monocular metric-depth provider. DefinesDepthProviderNode(Node)lazily insidemain(): subscribes a mono RGBsensor_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, defaultdepth-anything/DA3-SMALL— measured 0.27 GB / ~27 Hz on an 8 GB Ada), and republishes the returned metric depth as a32FC1Image (depth_topic) +CameraInfo(camera_info_topic) viadepth_convert, stamped indepth_frame_id. These are the topicsnvblox.launch.pyremaps onto nvblox'sdepth/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 atwarningand 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 puredepth_converthalf is unit-tested.packages/openral_slam_bringup/openral_slam_bringup/depth_height_filter_node.py—main(args=None) -> None— Nvblox floor-exclusion prefilter. DefinesDepthHeightFilterNode(Node)lazily insidemain(): subscribes a32FC1depth image +CameraInfo, derives a robot-relative navigation-height band fromrobot_yaml(RobotDescriptionfootprint, collision geometry, and link transforms), shifts it by the liveglobal_frame -> base_frameTF, looks up the depth optical frame inglobal_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 formapping_type: static_occupancy: Isaac ROS nvblox appliesstatic_mapper.workspace_bounds_*to TSDF view calculation but its camera occupancy integrator still projects raw floor returns into the 2D/map; the prefilter makes/mapa 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], andfilter_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 raiseValueErroron 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 overridesmin_height_m/max_height_m, andtf_timeout_ms. Launched bynvblox.launch.pybefore the NVIDIA component.packages/openral_slam_bringup/openral_slam_bringup/pycuvslam_node.py—main(args=None) -> None— In-process cuVSLAM visual SLAM (PyCuVSLAM wheel), stereo or mono RGBD. DefinesPyCuVSLAMNode(Node)lazily insidemain(): synchronizes a rectified stereo pair — or, whendepth_image_topicis set, one RGB camera + a metric-depth stream (mono RGBD,_build_mono_tracker/_on_rgbd: single-camera rig ≡ the RGB optical frame,OdometryMode.RGBDwithdepth_camera_id=0, depth encoded bydepth_to_uint16_mmatdepth_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 acuvslam.Rigfrom theCameraInfo(s), tracks withcuvslam.Tracker(enable_slamtoggles loop-closure SLAM vs pure VO; stereo only), publishesnav_msgs/Odometry(odometry_topic, map←rig), and broadcasts the samemap → odomTF edge the other SLAM backends fill (_emit) by composing the tracked pose with the liveodom ← rigTF at the image stamp. Two stereo rig modes (_build_tracker): (a) multi-camera — whenrig_frameis set, or derived fromrobot_yaml'sbase_frame— each camera'srig_from_cameraextrinsic 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 itsPmatrix (stereo_baseline_m), for a standalone RealSense-style rectified pair. The alternative to the composableisaac_ros_visual_slampath (cuvslam.launch.py) for hosts without the Isaac ROS apt stack — same NVIDIA engine from the operator-installed PyCuVSLAM wheel (never bundled; missing import raisesROSConfigErrorat startup). Pure helpers:stereo_baseline_m(right_p) -> float(raisesValueErrorunless the rectified rightPencodes a positive baseline),transform_to_pose(transform)(ageometry_msgs/Transform→rig_from_camerapose),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, anddepth_to_uint16_mm(msg, width, height, scale)(a32FC1metres Image → theuint16grid cuVSLAM RGBD eats: PIL float-bilinear resize onto the RGB resolution,metres × scaleclipped to uint16 — the inverse of cuVSLAM'sdepth_scale_factordivide; raisesValueErroron a non-32FC1encoding). 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 viapycuvslam.launch.py; deploy-wired forpanda_mobile_vslaminscenes/deploy/robocasa_vslam.yaml(stereo) andscenes/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.py—SafetyPassthroughNode(node_name="openral_safety")(L57),main(args=None)(L341) — Day-1 pass-through. Lifecycle node owning/openral/candidate_action → /openral/safe_actionplus/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 optionalclip(raw,-1,1) * cartesian_delta_scalephysical conversion as the C++ predictive kernel, so the two safety layers cannot disagree on normalized controller units.SafetySupervisorNodeis 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.py—RskillRunnerNode(*, 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 anopenral_runner.ROSPublishingHAL, subscribes to/openral/estopdefense-in-depth, and emits the F8 heartbeat.SkillResolveris the injected resolver type that returns a configured + activatedrSkillBase; production usesmake_default_skill_resolver(self)(branches onmanifest.kindto route VLAs to the local/HF-Hub path andros_action/ros_serviceskills toROSActionRskill). The local policy shim derives gripper slots fromRobotDescription.joints[].rolewhen a checkpoint exposes only action shape and applies optionalpolicy_extras.gripper_scalein 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 catchesROSRskillGoalSatisfiedspecifically and closes the goal withsuccess=True._run_until_done_or_deadline(...) -> strreturns why it exited ("completed"/"deadline"/"cancelled") — every exit used to be a barereturn, so a goal that blew its execution budget was reportedsuccess=True(observed live: a 144.5 s first inference against a resolved 45 s budget still closed SUCCEEDED). A"deadline"exit now aborts withfailure_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, overopenral_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 unconditionalsleep(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) -> boolowns the check, emitsopenral.event.deadline_missed, and records the true elapsed (the budget is only testable between steps, so a blockingskill.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 directexecute_rskilldispatch); 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 theirActionClient/ service client on it. Inspectsmanifest.kind:"vla"→make_local_skill_resolver(when in-tree) or_default_skill_resolver(HF Hub);"ros_action"/"ros_service"→ instantiatesROSActionRskill;"wam"→ raisesROSConfigError(not implemented).make_local_skill_resolver(search_paths, *, scene_cameras=()) -> SkillResolver— In-tree VLA resolver. Walks each search path once and indexes every*/rskill.yamlby manifest name; on resolve calls_build_runtime_skill_from_manifest. Acceptsros_node=Nonekwarg for signature uniformity with the default resolver.packages/openral_rskill_ros/openral_rskill_ros/compose.py—compose_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-treerobots/<id>/robot.yamlviaRobotDescription.from_yaml, builds a singleWorldStateAggregator, hands the same reference to a colocated_WorldStateLifecycleNodeandRskillRunnerNode, and always attachesSlamMapBridgeso any existing/mapOccupancyGrid publisher can populate the dashboard independently of the SLAM backend launch flag (slam_source_nodeoverrides the card'sopenral_slam_toolboxsource label — the visual backend setsopenral_nvblox).world_cloud_topicre-points the opt-inWorldCloudBridgefrom 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 aComposedRuntimedataclass withdescription,aggregator,world_state_node,rskill_runner_nodefields.packages/openral_rskill_ros/scripts/runtime_node— Composed-runtime entry point installed aslib/openral_rskill_ros/runtime_node. Reads ROS parameterrobot_yaml(absolute path), callscompose_runtime, and spins both lifecycle nodes on aMultiThreadedExecutor. Spawned once peropenral deploy sim/runinvocation bysim_e2e.launch.py(one generic launch — the per-robotopenarm_e2e.launch.py/so100_e2e.launch.pywere unified intosim_e2e.launch.pyin 2026-05-24). The dashboard SLAM Map bridge is now always part ofcompose_runtime; there is noenable_slam_bridgelaunch parameter. Paramsworld_cloud_topic+slam_source_node(both default"") thread the mono visual-SLAM dashboard wiring through tocompose_runtime(nvblox voxel-cloud topic + SLAM-card source label). Real deploys (openral deploy run): thedeploy_configparameter carries the DeployScene YAML path; when set the script opens every deploy-bound SensorSpec (robot manifest ∪ scenesensors:) viaopen_deploy_sensor_readersbefore spinning and closes them in the teardownfinally. Script top eagerly runsgi+Gst.init()BEFORE any numpy/pydantic/rclpy import — under Fast-DDS a laterrclpy.Node()segfaults when numpy/pydantic loaded first (x86 Ubuntu 24.04 / system PyGObject; bisected 2026-07-02)._prewarm_vla_imports() -> None(L49) — Importstorch+lerobot.policies.factory(the shared ~95% of any VLA adapter's import phase) insidephase_timer("vla_framework", prefix="prewarm"), emittingprewarm_vla_framework_{start,heartbeat,done}.except ImportError: passso slim installs skip cleanly. Second load-bearing ordering constraint in this file, alongside thegi/Gst.init()one above: the call must stay AHEAD ofopen_deploy_sensor_readers.transformers' import runsimportlib.metadata.packages_distributions()— astat()per file of every installed distribution — and eachstatreleases 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 neitherUV_COMPILE_BYTECODE=1norphase_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 ondeploy_config): sim spawns no in-process readers and so has no convoy, but the ~7 s import then overlaps the HAL's concurrenton_configure(~6 s MuJoCo arm / ~27 s cold robocasa) instead of being charged to the firstExecuteRskillgoal.packages/openral_rskill_ros/launch/sim_e2e.launch.py— Generic deploy ROS graph. Reasoner launch args are model-first:reasoner_model(defaultOPENRAL_REASONER_MODELor curatedgpt-5.5) and optionalreasoner_endpoint; these becomeOPENRAL_REASONER_{MODEL,ENDPOINT,MAX_TOKENS}on the reasoner node.reasoner_endpointtakes a named endpoint or a URL. The deprecatedreasoner_providerarg was removed in 0.3.0 with theOPENRAL_REASONER_LLM_*shim. Launch argworkcell_jsoncarries theDeployScenesafety/ACM subset when non-empty; the launch parses it, callscompute_intersection(robot, skill=None, deploy=...), appliesmerge_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_includescomposes the mono RGBD leg: pycuvslam in RGBD mode + the DA3 depth provider (framed at the camera's manifestframe_id) + nvblox — always, not gated on nav2), andslam_depth_sidecar_autostart(default true → anExecuteProcessrunstools/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 foropenral deploy run(the real-hardware counterpart of the sim HAL'sSimSensorBridge).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 throughmake_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 adeploy_bindingare skipped. Every camera publishes on<topic_prefix>/<name>/image:gstreamerbackends get the in-pipeline ROS tee forced on (publish_to_ros=True); other backends are wrapped in a pollingSensorRosPublisher, with manifest frame/intrinsics and companionCameraInfo. When the composed runtime's sharedaggregatoris passed, every reader also gets an_AggregatorPumpwriting frames directly intoWorldStateAggregator.update_image_frame; half-open legs are closed before errors propagate._AggregatorPump— daemon-thread pump (start/stop likeSensorRosPublisher) pollingread_latestat 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'ssensors.read_latestspan (modality/encoding/geometry/age + JPEG thumbnail) for a pump-fed camera, honouringOPENRAL_DASHBOARD_FLIP_180on a display copy only. Exists because_fallback_topic_rate_hzcaps the ROS tee that WorldState's_on_imagerides, 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 specrate_hz, else 10 Hz. This is the capture/native-tee cadence and is not capped._fallback_topic_rate_hz(spec) -> float—min(_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 isDEFAULT_STALENESS_S0.5 s, so 2 Hz would flap the diagnostics). Applies only to the PythonSensorRosPublisherfallback, whose every tick is a GIL-held rclpy Python→C conversion of a full-resolutionImage— 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'sDeployRuntimeat 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. ReturnsNone(native pixels) when the object detector is on (its node declaresinput_size640) 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 beforeslam_camera_names/topic_frame_sizeconsume it. The scene'senable_object_detector/enable_slamare 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.Noneoverrides keep the scene's value; no runtime block AND no overrides returnsNone(the callers' conservative native-pixels contract). Fed from theresolved_*bootstrap parameterssim_e2e.launch.pypasses toruntime_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 holdingreaders+publishers+direct_sensors(forward to WorldState'sdirect_image_frame_sensorsparameter;_on_imagethen 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 candidateActionChunks 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.estopraisesROSEStopRequested._row_major_flatten(rows) -> list[float](L49) — Private helper used by_action_to_chunk; preserves row-major ordering for the chunkflatarray._CONTROL_MODE_TO_UINT8: dict[ControlMode, int](L106) — Stable mapping fromopenral_core.ControlModeto the uint8 slot inActionChunk.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) — Defaultnav_msgs/OccupancyGridtopic slam_toolbox publishes on.encode_occupancy_grid_png(*, width: int, height: int, data: list[int]) -> str(L70) — Pure function rendering anOccupancyGrid.dataarray as a base64 PNG (unknown→mid-grey, free→white, occupied→black, in-between linear ramp; flipped so map-north points up). RaisesValueErroriflen(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 (zignored); delegates yaw toopenral_core.geometry.quat_xyzw_to_yaw. Used bySlamMapBridgeto project themap→base_framelookup 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 aslam.occupancy_gridspan (openral.slam.frame_id/width/height/resolution_m/origin_x/origin_y/png_b64/source_node, plusrobot_x/robot_y/robot_yaw/base_framewhen the tf2 lookup succeeds andfootprint_radius_mwhen 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 totopicwith slam_toolbox'sRELIABLE + TRANSIENT_LOCAL + KEEP_LAST=1QoS and prepares the OTel tracer.base_frameis 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 asopenral.slam.footprint_polygon_xyso the dashboard can draw the true oriented base outline (falling back to thefootprint_radius_mcircle).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) — Explicitfoxglove_bridgetopic_whitelist(camera images +/compressedsiblings,/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 bothfoxglove.launch.pyandsim_e2e.launch.pyso the allowlist has one source of truth.READ_ONLY_CAPABILITIES: list[str](topics.py L42) —["connectionGraph", "assets"]; omitsclientPublish/services/parametersso a connected viewer cannot publish, call services, or write params (cannot actuate).class MarkerSpec(bucket2_markers.py L58) — Frozen dataclass: onevisualization_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: convertopenral_msgs/WorldCollisionparallel capsule arrays to cylinder marker specs (length2·half_length, scale = diameter2·radius; sphere → zero-length cylinder). RaisesValueErroron 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 fromopenral_msgs/OccupancyVoxels(row-majoridx = x + size_x*(y + size_y*z),+0.5cell-centre offset). RaisesValueErrorwhenlen(occupancy) != size_x*size_y*size_z.class Bucket2MarkersNode(bucket2_markers.py L223) —rclpy.node.Nodesubscribing/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 aslib/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 paramsscore_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 aCriticWatchdogGroup+ aFailureBusPublisher(FailureSource.CRITIC); subscribesCriticScore(RELIABLE+VOLATILE+KL=10)._on_score(msg)routes the sample and publishes aKIND_CRITIC/SEVERITY_FAILFailureTriggeron 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 aslib/openral_reasoner_ros/critic_producer_node.py);rclpy.init→ spin → shutdown.