Skip to main content
STUB DOCUMENT: Expanded technical reference aligned with cyberwave-python; a human editor may tighten prose before publish.

What BaseDriver is

BaseDriver (cyberwave.driver) is the template-method lifecycle shell for Python edge drivers. You declare the platform contract once in define_interface(); the SDK compiles it to metadata["mqtt"] (and optional metadata["zenoh"]), wires MQTT at runtime, and runs a fixed async lifecycle.
Do not override run_async(). Override hooks and define_interface() instead.
For ROS 2 robots, use BaseROS2Driver instead — it subclasses BaseDriver and adds rclpy lifecycle + from_ros forwarding.

Construction

Edge Core / Docker (env-driven)

Edge Core injects CYBERWAVE_TWIN_UUID, CYBERWAVE_API_KEY, MQTT settings, etc. The driver typically uses:
from_env() calls Params.from_env() when your subclass defines Params; otherwise cls(None) and twin UUID comes from CYBERWAVE_TWIN_UUID during cloud connect.

Constructor kwargs

On __init__, the SDK always:
  1. Creates DriverInterfaceRegistry, AlertManager, BaseTelemetry
  2. Calls define_interface_defaults(iface) (built-in commands + telemetry publisher)
  3. Calls define_interface(iface) (your declarations)

Required vs optional (checklist)

You must implement (abstract)

Pure forward-only ROS drivers often leave the four async on_* hooks as no-ops on BaseROS2Driver (see ROS page); on plain BaseDriver they are still abstract and must exist (can pass).

You usually implement

Optional overrides (defaults are fine)


Full lifecycle (run_async)

Step-by-step (what happens without your code)

  1. CONFIGURING — Log [STATE] unconfigured → configuring. Raise lifecycle alert + twin notice “Driver starting”.
  2. _connect_cloud_async()Cyberwave(source_type="edge") (SDK defaults + CYBERWAVE_* env). Connect MQTT. Resolve twin: reuse twin= or fetch by robot_uuid / asset.
  3. _start_driver_telemetry_session() — Publish telemetry_start + connected on MQTT; start debounced driver_info via BaseTelemetry.
  4. register_interface_on_twin() — If auto_register_interface and class ASSET_KEY are set: twin.driver.set_schema(self.manifest) (failures are logged, driver continues).
  5. on_configure() — Your hook.
  6. CONNECTING — Twin notice “Driver connecting to device”. on_connect_to_device().
  7. INACTIVE — Notice “Driver wiring interfaces”. on_register_callbacks() then _wire_interface_from_registry():
    • Subscribes MQTT listeners for add_listener entries (respecting operation_modes)
    • Schedules tick publishers for add_publisher entries with PublisherArgs.rate_hz
    • Does not subscribe ROS topics (use BaseROS2Driver + from_ros)
  8. on_activate() then _activate_registry_zenoh() — Opens edge DataBus for dual/Zenoh specs.
  9. ACTIVE — Notice “Driver active” + DRIVER_ACTIVE alert. Then _derive_initial_operation_mode() sets the starting mode from the twin’s attached controller (see Operation modes), and asyncio.gather(_tick_loop_async, on_start_monitoring, _reconnect_loop_async).
  10. Shutdownrequest_shutdown() sets _shutdown event. Unwire MQTT, end telemetry session, on_shutdown(), flush AlertManager, FINALIZED.
On unhandled exceptions: state ERROR, then same finally teardown.

Lifecycle states (DriverLifecycleState)

Each transition:
  • Logs INFO [STATE] from → to
  • Updates debounced driver_info (lifecycle_state field)
  • Raises DRIVER_LIFECYCLE via AlertManager (resolved on next transition)
  • On active: also raises DRIVER_ACTIVE (resolved on deactivate/finalize/error)
  • Calls create_twin_alert for the UI when _twin is available (queued if twin not ready yet, flushed after connect)
Backend alert integration is always enabled after MQTT connect (not configurable).

define_interface and the registry

Call order at import/construction

What define_interface_defaults registers

Handlers are wired to internal methods (_on_stop_cmd, …) that update operation_mode and re-wire listeners when modes change.

Declaring your own entries

on_register_callbacks is not for MQTT. Registry wiring happens in _wire_interface_from_registry() after your hook returns. Use on_register_callbacks for hardware/SDK subscriptions only.
twin/command must stay MQTT. Do not set enable_zenoh=True on command listeners.

Operation modes (DriverOperationMode)

Orthogonal to lifecycle state — gates which add_listener / add_publisher entries are active. An entry is wired only when the driver’s current mode is in its operation_modes set. Initial mode is derived from the twin’s attached controller, not hard-coded. At the end of startup (just after ACTIVE), run_async calls _derive_initial_operation_mode(), which re-fetches the twin from the API (refresh_driver_twin_from_api) and reads its attached controller policy (resolve_twin_attached_controller): When a controller is resolved, the driver calls on_controller_assigned(ctype, policy_uuid) (override to add driver-specific alerts/setup). Runtime switches come through the default management commands (controller-changed, teleoperate, remoteoperate, stop). Each routes to _set_operation_mode(mode), which — only when the mode actually changes — re-wires the interface registry (and ROS from_ros forwarders), then calls the matching enter hook and emits a driver_info update with the new operation_mode: Read the live mode via the operation_mode property. Because _set_operation_mode is a no-op when the mode is unchanged, a forward-only driver with no controller stays in its initial NO_OP and never fires an enter hook — declare such publishers with operation_modes=frozenset(DriverOperationMode) so they stream in every mode.

Stream publish rate limiting

High-frequency sources (sensor topics, IMUs) can exceed 100–200 Hz; publishing every sample to MQTT/Zenoh overloads the broker. BaseDriver provides a per-stream monotonic throttle:
Tick-based registry publishers are already paced by PublisherArgs.rate_hz; this throttle is for callback-driven streams you publish yourself. BaseROS2Driver adds a ROS-topic-aware wrapper — see the ROS page.

Source-type convention

Every state payload on the Cyberwave bus carries a source_type that says who produced it. This is how the platform keeps a driver’s own feedback from being mistaken for a command (and vice versa). The basic contract for a driver is simple:
Publish your state as edge (or sim). Listen for teleop commands (tele, edit, sim_tele).

Publishing (outbound)

Tag outbound state with edge by default, or sim when the driver is driving a simulator rather than hardware. For from_ros publishers the SDK stamps edge for you (see the ROS page); for hand-built publisher callbacks, include "source_type": SOURCE_TYPE_EDGE in the returned dict.

Listening (inbound)

Declare the command source types a listener accepts with ProtocolArgs:
The acceptance rule a listener should apply to each inbound message:
The edge* self-echo guard is the one rule you cannot relax. When the same topic is both an edge publisher and a tele listener — common for an arm’s joint/update — accepting edge inbound would feed the driver its own joint feedback as a command and the arm would fight itself.

What the tick loop does

During ACTIVE, _tick_loop_async runs at TICK_RATE_HZ (class attribute, default 10 Hz):
  1. _run_registry_publishers() — For each publisher with PublisherArgs.rate_hz, if the tick counter matches the rate, invoke callback and publish to MQTT and/or Zenoh (when enable_zenoh=True).
  2. on_tick() — Your extra periodic work.
from_ros publishers do not use this loop — ROS message rate drives publishing (ROS driver only).

Identity: twin, asset, environment


Manifest export (no run())

Compiled bundles include top-level asset_key from ASSET_KEY. Probe instances use _manifest_probe() — no MQTT, no lifecycle.

Environment variables (cloud shell)

Loaded by CyberwaveConfig when the driver calls Cyberwave(source_type="edge") (same as the rest of the SDK):

Alerts and operator visibility

create_twin_alert from the asyncio loop dispatches HTTP in a background thread so ticks are not blocked.

MQTT reconnect

If RECONNECT_MAX_ATTEMPTS > 0 (default 5) and you override on_reconnect() to return True when transport is restored, _reconnect_loop_async reacts to _connection_lost (set this from your device code on disconnect). Default on_reconnect returns False — reconnect is effectively off; repeated failures can move to ERROR. Set RECONNECT_MAX_ATTEMPTS=0 to disable the watcher entirely.

Zenoh (edge-colocated)

Opt in per publisher via TopicSpec fields:
At on_activate, _activate_registry_zenoh() opens data_bus_for(twin_uuid) and publishes registry streams locally. High-rate binary (camera) often uses ZenohPublisherMixin instead of tick publishers.

Class attributes reference


Anti-patterns


BaseROS2Driver

rclpy lifecycle, from_ros, UR Sim lab.

Writing compatible drivers

Platform contract, cw-driver.yml, tutorials.

Driver alerts

Alert codes and operator surfaces.