What BaseROS2Driver is
BaseROS2Driver (cyberwave.driver.ros2) combines:
BaseDriver— Cyberwave API/MQTT, twin binding, interface registry, lifecycle alerts,driver_infotelemetryrclpy.lifecycle.Node— standard ROS 2 lifecycle transitions
ursim_driver.py.
Video walkthrough — Add your hosted recording when available:
[UR Sim → Cyberwave: build a ROS 2 driver in minutes](VIDEO_URL_HERE)Process architecture (two event loops)
BaseROS2Driver.run() is not a plain BaseDriver.run():
ROS callbacks (e.g.
/joint_states) run on the executor thread; they asyncio.run_coroutine_threadsafe into the driver loop to publish MQTT without blocking rclpy.
The ROS 2 lifecycle (managed) node
BaseROS2Driver is a ROS 2 managed node — it subclasses rclpy.lifecycle.Node (a LifecycleNode) rather than a plain rclpy.node.Node. A managed node does not start doing work the moment it is constructed; it advances through an explicit, tool-controllable state machine so an orchestrator (or, here, the Cyberwave lifecycle) can bring hardware up and down deterministically.
The four primary states (ROS 2 design REP-2007):
Transitions (the verbs that move between states) are where your code runs. rclpy calls a
on_<transition>(state) method for each; returning SUCCESS advances the state, FAILURE/ERROR does not:
create_publisheron a LifecycleNode returns a lifecycle publisher that only actually transmits while the node is Active. Publishing inInactiveis silently dropped — this is by design, not a bug.- rclpy owns the
on_configure(state)/on_activate(state)/on_deactivate(state)/on_shutdown(state)methods. You must not redefine them with a different signature — that is exactly why Cyberwave exposes the prefix-droppedconfigure()/activate()/ … hooks (see Why hook names drop theon_prefix below). - Who triggers transitions? Either an external lifecycle manager (
ros2 lifecycle set <node> configure) or, more commonly here, the driver itself whenCW_ROS2_AUTO_ACTIVATE=true(it calls its own~/change_stateservice after the cloud connect). Without auto-activate the node sits in Unconfigured until something activates it. on_cleanupis treated as fatal in this base — recovering an edge device in-process is unreliable, so the container is expected to restart to reconfigure (see the transition table below).
DriverLifecycleState), and the two are coordinated — that coordination is the next section.
Two lifecycles — who calls what
You maintain two parallel state machines. They are ordered: Cyberwave connect andon_configure run before the driver waits for ROS ACTIVE; MQTT + from_ros wiring runs after ROS activate signals ready.
Cyberwave side (run_async — do not override)
ROS side (rclpy — do not override on_configure(state), etc.)
Why hook names drop the on_ prefix
rclpy.lifecycle.Node already defines on_configure(state), on_activate(state), … with a state argument. A no-arg def on_configure(self) in your subclass would break ROS lifecycle.
Cyberwave
on_configure, on_connect_to_device, … still exist on the class for BaseDriver’s ABC, but BaseROS2Driver routes them through _BaseDriverAsyncHooks — you normally do not implement them for forward-only drivers.
Event hooks keep on_ where there is no conflict: on_topic_name_changed(topic_entry, new_name).
Required vs optional (checklist)
Minimum for ROS → Cyberwave streaming (UR Sim style)
You do not need:
- Manual MQTT
publishloops forfrom_rosentries - Custom
JointState→ dict conversion (SDK default) - Overriding rclpy
on_activate(state) - Implementing Cyberwave abstract
on_configureasync methods (mixin provides them)
When you need more
define_interface and from_ros
add_publisher with ROS source
What wire_ros_publishers() does at runtime
- Lists registry entries with
from_rosfor currentoperation_mode resolve_ros_message_class— polls ROS graph (or usesmsg_type=override)- Creates
create_subscriptionon the ROS topic - On each message:
- If no user callback and type is
sensor_msgs/msg/JointState→ros_joint_state_to_transport_payload - Else if user callback → must return
dict - Else →
ros_message_to_transport_payload(generic field flattening +timestamp,source_type)
- If no user callback and type is
run_coroutine_threadsafe→ publish to resolved MQTT slug (and Zenoh if dual spec)
ROS forward: /joint_states (sensor_msgs/msg/JointState) -> Cyberwave MQTTROS RX /joint_states: N message(s) — publishing to Cyberwave MQTT (6 joints)
Joint /update payload shape (default)
Flat keys for Vector / twin telemetry compatibility:
positions: { name: value } is available via ros_joint_state_to_transport_payload(..., aggregated=True) if you supply a custom callback.
Source-type convention
Notice the"source_type": "edge" field above. This is the platform-wide convention every driver follows (full rules on the BaseDriver page):
from_rospublishers stampedgeautomatically (physical feedback). When the driver runs against a simulator instead of hardware, publishsiminstead.- Command listeners (
add_listener+ros_publish) should accept teleop sources —tele,edit,sim_tele— and must never act onedge*messages, or the driver re-ingests its own joint feedback and fights itself. Declare the accepted set withProtocolArgs(source_types=[...]). - Inbound messages without a
source_typeare treated leniently (accepted as commands) — not every producer stamps the field — but theedge*self-echo guard always applies.
from_ros publisher (edge) and a command listener (tele/edit/sim_tele) — e.g. joint/update on an arm — is the canonical case where the edge* rejection matters.
CW_ROS2_AUTO_ACTIVATE
UR Sim example sets default in
main():
ros2 lifecycle set …) while the executor is spinning.
Operation modes drive the ROS lifecycle
BaseROS2Driver ties Cyberwave operation modes to ROS lifecycle transitions. When _set_operation_mode runs, the ROS enter-hooks request a change_state transition on this node:
So a twin with no controller assigned starts in
NO_OP: control is idle, but the node stays warm. Because _set_operation_mode is a no-op when the mode is unchanged, a forward-only bridge (no controller) never deactivates — it remains in its initial NO_OP and keeps streaming.
Edge ROS→Cyberwave forwarders are independent of operation mode.
wire_ros_publishers() runs as soon as ROS reaches ACTIVE, so from_ros joint/telemetry streams flow even in NO_OP. Declare them with operation_modes=frozenset(DriverOperationMode).on_controller_assigned / on_controller_removed) to add hardware enable/disable on top of the lifecycle transition — e.g. powering an arm when a controller attaches.
Managed vendor launch (managed_launch)
A driver can have the SDK spawn and supervise a vendor ros2 launch (e.g. a robot’s stock bringup) as a child process, instead of you starting it separately. Declare it in define_node_manifest():
activate, ManagedRosLaunch starts the child, waits for readiness, and stops it on shutdown. Setup scripts are sourced into the driver process so vendor message typesupport imports correctly.
ROS stream → Cyberwave rate limiting
ROS sensor topics often exceed 100 Hz. Thefrom_ros forwarder and any custom ROS callback can throttle outbound Cyberwave publishes with a per-topic cap (default 50 Hz):
BaseDriver.acquire_stream_publish_slot with a ROS-topic key. See BaseDriver → Stream publish rate limiting.
Combined manifest (ROS params + MQTT catalog)
BaseROS2Driver exports a single manifest.yaml that merges the ROS node manifest (params, topics, managed_launch) with the compiled-from-code MQTT cw-driver catalog:
Export it without running the driver — typically a CLI subcommand and a Docker build step:
define_node_manifest + define_interface) and re-export. At runtime, point the driver at it with CW_DRIVER_MANIFEST.
Construction and entrypoint
Node name / namespace can be overridden via ROS env helpers (
node_name_from_env, node_namespace_from_env).
Environment variables
Cyberwave (inherited from BaseDriver)
Backend lifecycle alerts are always synced after connect (not env-configurable). Set
ASSET_KEY on the driver class (not via env).
ROS 2 driver specific
Manifest may declare
tick_rate_hz ROS parameter (integer) — drives ROS-side tick() timer. Per-parameter overrides also follow the SDK CW_ROS2_* env convention.
The SDK no longer defaults
ROS_SETUP to a specific distro. For managed_launch drivers, set ros_setup in the manifest (recommended) or export ROS_SETUP / ROS_SETUP_OVERLAY.ROS hooks — what to put in each
configure()
- Parse non-hardware config, allocate buffers
- Do not open device connections (that is
connect_to_device()) - rclpy
on_configurealready declared manifest parameters
connect_to_device()
- Open robot/sim connection if needed
- For UR Sim +
ur_robot_driver, often empty — another node publishes/joint_states
register_callbacks()
- Device-native ROS subscriptions not covered by
from_ros - Example: subscribe to a custom diagnostic topic with a Python callback
activate()
- Create ROS publishers you control manually
- Start control loops
from_roswiring happens after this inrun_async— do not rely on MQTT forwards duringactivate()itself
tick()
- Fast periodic work on ROS timer
- Exceptions are logged; timer keeps running
shutdown()
- Destroy publishers/subscriptions you created
- Complement ROS
on_shutdowntransition
Cyberwave → ROS (commands)
ros_publish caches publishers per (topic, msg_type).
Reference driver: UR Sim (ursim_driver.py)
Full listing (~75 lines) in the monorepo. Structure:
main() sets logging, CW_ROS2_AUTO_ACTIVATE, reads optional manifest/node env, calls driver.run().
Lab setup (UR Sim)
End-to-end Docker, pendant program,ur_robot_driver, and running ursim_driver.py: UR Sim tutorial.
Protocol layers (TCP, ROS DDS, MQTT): Custom integrations.
from_ros vs other patterns
Troubleshooting
Related
BaseDriver
Cyberwave-only lifecycle, registry, manifests, alerts.
Writing compatible drivers
cw-driver.yml, env contract, fake-IMU tutorial.Driver alerts
Alert types during connect and failures.