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. ADR-0018 F8 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. ADR-0018 F8 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. ADR-0018 F2: publishes typedopenral_msgs/WorldStateStampedon/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_statetopic removed. Module exposesTOPIC_FAST,TOPIC_SLOW, andbuild_world_state_stamped_msg(node, world_state) -> WorldStateStamped(L849) — pure-Python translation ofopenral_core.WorldStateto the typed message (sorted parallel arrays for determinism;trace_idset from current OTel context viaopenral_observability.propagation.current_traceparent). ADR-0035:build_world_state_stamped_msgnow also serialises the spatial memory onto thedetected_object_*parallel arrays (labels / confidences /geometry_msgs/Pointpositions /int32track-ids (-1= unset) / frame) via the private helper_fill_detected_objects(msg, world_state) -> None(L999), so separate-process consumers of/openral/world_state_slownow seeWorldState.detected_objectson the wire.__init__accepts an optional pre-constructedWorldStateAggregatorso F1's compose factory can share one instance withrskill_runner_node(ADR-0018 §3 — single shared aggregator).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, since ADR-0035, 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) — ADR-0035 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, ADR-0037 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 (ADR-0051) —_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 (ADR-0043): 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 (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, 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 (ADR-0050) —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 ADR-0035 "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— ADR-0047 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 (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_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.packages/openral_perception_ros/openral_perception_ros/reward_monitor_node.py—main(args=None) -> None— ADR-0057 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 aRobometerRewardbackend from akind:"reward"manifest_path(_build_monitor→build_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_progress→RobometerReward.assess); returnsok=False, stale=Truewhen no fresh frame / no task. The reward counterpart ofscene_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 offersquery_task_progresswhentask_progress_availableis set. Advisory-only — no actuation.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_safety/openral_safety/supervisor_node.py—SafetyPassthroughNode(node_name="openral_safety")(L57),main(args=None)(L341) — ADR-0018 F5 Day-1 pass-through. Lifecycle node owning/openral/candidate_action → /openral/safe_actionplus/openral/estop+/openral/estop_reset(std_srvs/Trigger). Day-1 envelope checks:n_dofmismatch and per-joint position-limit on the first chunk row.SafetySupervisorNodeis a back-compat alias. The constantDEFAULT_ESTOP_RESET_COOLDOWN_S = 0.5seeds theestop_reset_cooldown_sparameter.
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.py—RskillRunnerNode(*, 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 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)(ADR-0024 — branches onmanifest.kindto route VLAs to the local/HF-Hub path andros_action/ros_serviceskills toROSActionRskill). The execute loop catchesROSRskillGoalSatisfiedspecifically and closes the goal withsuccess=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 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 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.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) -> ComposedRuntime(L69) loads any in-treerobots/<id>/robot.yamlviaRobotDescription.from_yaml, builds a singleWorldStateAggregator, and hands the same reference to a colocated_WorldStateLifecycleNodeandRskillRunnerNode. Honors ADR-0018 §3 "only subscriber of /joint_states."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 siminvocation 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).
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 structuralopenral_hal.protocol.HALProtocol.connectopens a publisher oncandidate_action_topicand subscribes tojoint_state_topic;read_statereturns the latest cachedJointState;send_actionserialises theActionPydantic intoopenral_msgs/ActionChunk(row-majorflat,trace_idfrom current OTel context,rskill_id/rskill_revisionfrom the injected getters) and publishes.estopraisesROSEStopRequested._row_major_flatten(rows) -> list[float](L47) — Private helper used by_action_to_chunk; preserves row-major ordering for the chunkflatarray._CONTROL_MODE_TO_UINT8: dict[ControlMode, int](L107) — Stable mapping fromopenral_core.ControlModeto the uint8 slot inActionChunk.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) — Defaultnav_msgs/OccupancyGridtopic slam_toolbox publishes on.encode_occupancy_grid_png(*, width: int, height: int, data: list[int]) -> str(L75) — 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.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 (zignored); delegates yaw toyaw_from_quaternion_xyzw. Used bySlamMapBridgeto project themap→base_framelookup 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 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)(L234) — 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(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) — 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.