Skip to main content

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 REGISTRY_ID 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. Shutdown — request_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.

Command parameters

In a supporting SDK release, declare the values a user must provide with a command:
Pass this declaration to your existing listener. Keyboard setup in Catalog and Workbench shows the declared choices. Commands with missing required values remain available for configuration, without receiving an automatic key binding. Validate hardware limits and the command itself in your driver before executing it. The original CommandArg(name, default, unit) form keeps its existing behavior. Typed fields are optional and keyword-only; min_items declares a minimum array length. Numeric zero and boolean false defaults are retained. Adopt typed declarations deliberately with a compatible backend; declaring parameters does not make a device ready to move. After refreshing the twin, inspect its current commands before changing an existing typed declaration. Save the revision you reviewed and pass it with the update:
If the update returns 409, refresh and review again. Do not automatically retry a changed configuration. Existing untyped declarations keep their current behavior.

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:
Resolution order for the effective cap (highest wins): a per-publisher TopicSpec(rate_hz=...) on the from_ros/add_publisher entry → the node manifest’s top-level mqtt_max_hz: value → the STREAM_PUBLISH_MAX_HZ class default. No environment variable controls this. 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 REGISTRY_ID. 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.

Edge health (twin liveness)

BaseDriver automatically starts an EdgeHealthCheck publisher on cyberwave/twin/{uuid}/edge_health right after the MQTT interface is wired, and stops it on shutdown — no lifecycle wiring required.
cyberwave-edge-core also runs its own EdgeHealthCheck per linked twin for host-level metrics (memory, CPU temp, watchdog). A driver container’s edge_health_extras() is a stopgap reuse of the same publish mechanism for driver-specific fields — a dedicated per-driver telemetry channel is planned to replace it.

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.