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.
For ROS 2 robots, use BaseROS2Driver instead — it subclasses
BaseDriver and adds rclpy lifecycle + from_ros forwarding.
Construction
Recommended (notebook, tests, explicit twin)
Edge Core / Docker (env-driven)
Edge Core injectsCYBERWAVE_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:
- Creates
DriverInterfaceRegistry,AlertManager,BaseTelemetry - Calls
define_interface_defaults(iface)(built-in commands + telemetry publisher) - 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)
CONFIGURING— Log[STATE] unconfigured → configuring. Raise lifecycle alert + twin notice “Driver starting”._connect_cloud_async()—Cyberwave(source_type="edge")(SDK defaults +CYBERWAVE_*env). Connect MQTT. Resolve twin: reusetwin=or fetch byrobot_uuid/ asset._start_driver_telemetry_session()— Publishtelemetry_start+connectedon MQTT; start debounceddriver_infoviaBaseTelemetry.register_interface_on_twin()— Ifauto_register_interfaceand classASSET_KEYare set:twin.driver.set_schema(self.manifest)(failures are logged, driver continues).on_configure()— Your hook.CONNECTING— Twin notice “Driver connecting to device”.on_connect_to_device().INACTIVE— Notice “Driver wiring interfaces”.on_register_callbacks()then_wire_interface_from_registry():- Subscribes MQTT listeners for
add_listenerentries (respectingoperation_modes) - Schedules tick publishers for
add_publisherentries withPublisherArgs.rate_hz - Does not subscribe ROS topics (use
BaseROS2Driver+from_ros)
- Subscribes MQTT listeners for
on_activate()then_activate_registry_zenoh()— Opens edgeDataBusfor dual/Zenoh specs.ACTIVE— Notice “Driver active” +DRIVER_ACTIVEalert. Then_derive_initial_operation_mode()sets the starting mode from the twin’s attached controller (see Operation modes), andasyncio.gather(_tick_loop_async, on_start_monitoring, _reconnect_loop_async).- Shutdown —
request_shutdown()sets_shutdownevent. Unwire MQTT, end telemetry session,on_shutdown(), flushAlertManager,FINALIZED.
ERROR, then same finally teardown.
Lifecycle states (DriverLifecycleState)
Each transition:
- Logs
INFO [STATE] from → to - Updates debounced
driver_info(lifecycle_statefield) - Raises
DRIVER_LIFECYCLEviaAlertManager(resolved on next transition) - On
active: also raisesDRIVER_ACTIVE(resolved on deactivate/finalize/error) - Calls
create_twin_alertfor the UI when_twinis available (queued if twin not ready yet, flushed after connect)
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
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:
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 asource_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 asedge(orsim). Listen for teleop commands (tele,edit,sim_tele).
Publishing (outbound)
Tag outbound state withedge 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 withProtocolArgs:
What the tick loop does
During ACTIVE,_tick_loop_async runs at TICK_RATE_HZ (class attribute, default 10 Hz):
_run_registry_publishers()— For each publisher withPublisherArgs.rate_hz, if the tick counter matches the rate, invoke callback and publish to MQTT and/or Zenoh (whenenable_zenoh=True).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 byCyberwaveConfig 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
IfRECONNECT_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 viaTopicSpec fields:
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
Related
BaseROS2Driver
rclpy lifecycle,
from_ros, UR Sim lab.Writing compatible drivers
Platform contract,
cw-driver.yml, tutorials.Driver alerts
Alert codes and operator surfaces.