Skip to content

ROS 2

The ROS 2 bridge allows you to put a live OmniLoop control plane on a running robot. This is where OmniLoop’s halt, live-mutate, and no-restart capabilities matter most — bringing up or debugging real hardware, not just tuning a simulation.

The bridge handles data in three directions:

  • Telemetry (Robot -> Dashboard): Subscribes to topics; each message is automatically flattened into scalar readouts.
  • Control (Dashboard -> Robot): Exposes parameters as sliders or toggles; edits in the dashboard are pushed as ROS 2 parameter updates.
  • Lifecycle (Dashboard -> Robot): Drives managed (rclcpp_lifecycle) nodes through their state machine — configure / activate / deactivate / cleanup — from the dashboard, with the live state reflected back.

Start the bridge with your desired telemetry topics and parameters:

import rclpy
from omniloop.integrations.ros2 import OmniLoopRosBridge
rclpy.init()
bridge = OmniLoopRosBridge(
node_name="omniloop_bridge",
telemetry_topics=[
("/imu/base_vel", "std_msgs/msg/Float64")
],
params=[
dict(name="max_speed", type="float", kind="slider",
min=0.0, max=2.0, step=0.05, default=1.0, target_node="/controller")
],
lifecycle_nodes=["/controller"], # drive a managed node from the dashboard
)
bridge.spin() # Blocks; publishes telemetry and applies dashboard edits
  • Message Flattening: OmniLoop automatically unwraps standard messages (like std_msgs .data) and walks nested fields to create clean, readable scalars in the dashboard.
  • Parameter Push: When you move a slider in the dashboard, the bridge uses an AsyncParameterClient to dynamically update the parameter on the target node.

Pass lifecycle_nodes=[...] to drive managed nodes from the control plane. Each entry is a node name ("/controller") or a dict ({"node": "/controller", "label": "Controller"}). For each managed node the bridge adds two dashboard controls under a Lifecycle group:

  • a transition control — set its value to configure, activate, deactivate, cleanup, or shutdown to request that transition;
  • a readonly state readout (<node>_lifecycle_state) reflecting the node’s live primary state (unconfigured / inactive / active / finalized).

Transitions are issued through each node’s change_state service (asynchronously, so a timer callback never blocks the executor), and state is polled via get_state. Because these are ROS 2 service calls — not node teardown — a deactivate/activate cycle does not destroy the bridge’s telemetry subscriptions (nor the managed node’s own publishers/subscriptions, which live across the inactive state by design).

For scripted control outside the dashboard, LifecycleController wraps the same services directly:

from omniloop.integrations.ros2 import LifecycleController
lc = LifecycleController(node)
lc.transition("/controller", "configure")
lc.transition("/controller", "activate")
lc.request_state("/controller", lambda state_id, label: print(label))
Parameter Type Description
lifecycle_nodes List Managed nodes to drive: a node name or {"node", "label"} dict.
lifecycle_poll_hz Float How often to refresh managed-node state readouts (default 2.0).