Skip to main content

What is a compatible driver?

A compatible driver connects your hardware’s native API to a Cyberwave asset and twin: it translates the device’s protocol into MQTT topics, command envelopes, and (optionally) edge data channels that the UI, workflows, and SDK understand. This guide is for integrators building a driver from scratch for a custom or third-party asset in your workspace. Cyberwave-maintained drivers use the same rules; their cw-driver.yml files are linked later as examples only. Drivers can run on edge hardware, on the robot, in the cloud, or on a developer laptop. In production they usually run beside the device under Edge Core. Each driver ships as a Docker image. Edge Core pulls and runs it with the environment variables below, so local development matches production deployment.

Choose a starting path

UR Sim (ROS 2) — recommended

End-to-end Docker lab + ursim_driver.py. SDK concepts: Python SDK drivers below.

Fake IMU (no hardware)

Synthetic IMU on a D455 twin — manifest, commands, MQTT + optional Zenoh before real sensors.

Custom protocol

BaseDriver from scratch — serial, SDK, WebRTC, or hand-written MQTT.
Deep references: BaseDriver · BaseROS2Driver

Quickstart: Python SDK

Use the cyberwave package and BaseDriver in cyberwave.driver. You declare topics and commands in Python; the SDK generates the MQTT catalog and wires runtime subscriptions.

BaseDriver reference

Lifecycle, hooks, manifest export, alerts, and what the SDK runs for you.

BaseROS2Driver (ROS 2)

Dual lifecycle, from_ros forwarding, hooks — often under 100 LOC for ROS → twin streaming.
get_manifest() builds the compiled MQTT catalog from define_interface() only — no twin connect, no run(). Use get_manifest_bundles() when you also declare Zenoh channels (see Zenoh edge catalog). ROS 2 integrators: start with the UR Sim tutorial, then read Python SDK drivers below for what the SDK handles vs what you implement. Otherwise see the fake IMU tutorial, then reference for cw-driver.yml, REST/MQTT, and sensor buses. (C++ drivers use the same platform contract via cw-driver.yml; see the C++ SDK.)

Python SDK drivers

The cyberwave package provides base classes so you declare the platform contract once in Python instead of hand-wiring MQTT and catalog JSON.

What you declare vs what the SDK runs

Source-type convention. A driver publishes its state as edge (or sim when driving a simulator) and listens for teleop commands — tele, edit, sim_tele. It must never act on edge* messages (that’s its own feedback), and inbound messages with no source_type are accepted leniently. Full rules: BaseDriver → Source-type convention.A driver with no controller assigned starts in NO_OP — control commands stay idle while edge telemetry/from_ros forwarders keep streaming. Declare always-on publishers with operation_modes=frozenset(DriverOperationMode). See Operation modes. High-rate streams should throttle with TopicSpec.rate_hz on from_ros publishers (or acquire_stream_publish_slot() in custom callbacks; default cap 30 Hz). ROS drivers can also have the SDK spawn a vendor ros2 launch (managed_launch) and export a combined ROS+MQTT manifest.yaml (write-manifest) — see the BaseROS2Driver reference.

Driver mixins (arm + joints)

For manipulator arms, compose SDK mixins instead of hand-wiring MQTT joint teleop, arm commands, and ROS feedback. See Driver mixins and capabilities for the full inventory and a Piper walkthrough. Bidirectional joint/update: call super().define_interface(iface) first (mixins register the teleop listener); then add your edge publisher with from_ros. Clients use twin.joints.set() for commands and twin.joints.get() to read feedback — drivers use the registry, not twin.joints.set().

Integration example: PiperDriver

Reference: cyberwave-edge-nodes/piper/piper_driver.py.
Motion shaping (trapezoidal vs passthrough): Joint motion shaping.

Driver mixins reference

MRO, seams, Piper-style ROS arm pattern.

Joint motion shaping

joint_controller_motion() — shaped vs passthrough.

ROS 2 minimal example (ursim_driver.py)

Flagship sample: examples/ursim_driver.py (~75 lines).
Run after ROS and UR Sim are up (full lab): UR Sim tutorial.

UR Sim tutorial

Docker, pendant program, ur_robot_driver, run ursim_driver.py, GIFs.

Custom integrations

TCP, ROS DDS, MQTT — how the lab stack fits together.

BaseROS2Driver reference

Dual lifecycle, env vars, ros_publish() for commands.

BaseDriver reference

Non-ROS drivers, manifest export, hooks.

Driver mixins

Arm + joint mixin composition, bidirectional joint/update.

Tutorial: build your first driver (fake IMU)

Reference implementation: cyberwave-python/examples/fake_imu_driver.py — hardware-free, useful to validate manifest + MQTT + twin.imu.get() before real sensors. ROS 2 arms: use the UR Sim tutorial and Python SDK drivers.

Step 1 — Class skeleton and identity

ASSET_KEY ties the driver to a catalog asset. TICK_RATE_HZ drives both on_tick and registry publisher scheduling.

Step 2 — define_interface (manifest + runtime contract)

This single method feeds get_manifest() / driver.manifest, optional metadata["zenoh"], and _wire_interface_from_registry() (MQTT subscribe/publish plus edge DataBus when dual specs are present). twin/command stays MQTT-only — do not set enable_zenoh=True on command listeners.

Step 3 — Lifecycle hooks (map to run_async order)

For a real camera driver, on_connect_to_device opens the device SDK and on_register_callbacks attaches frame callbacks.

Step 4 — Publisher callback (MQTT stream)

BaseDriver calls this from _run_registry_publishers() at 50 Hz and publishes to cyberwave/twin/{uuid}/imu. You do not call mqtt.publish yourself for registry publishers.

Step 5 — Command callback (rotate)

twin.commands.rotate(...) publishes to cyberwave/twin/{uuid}/command; the registry routes it here while the driver is ACTIVE. Expected logs: inactive → active, IMU stream active, then command received, rotating sensor on the driver logger; imu.get() shows higher gyro.z after yaw rotate (~0.52 rad/s + noise). rotate is a sensor command (not locomotion) — no teleop policy required on camera-only twins.

Minimal skeleton (any driver)

Registry API reference

Session markers (telemetry_start, connected, …) use client.mqtt directly. Queue operational fields with emit_driver_info(); the registry telemetry publisher flushes them.

Twin SDK: twin.driver vs twin.commands

After you register a catalog with twin.driver.set_schema(...): The fastest way to get started is the Cyberwave Driver skill for Claude Code. It asks you a few questions about your hardware and scaffolds a complete, production-ready driver project — including the Dockerfile, local dev setup, and a working twin connection. Install the skill:
Then in any Claude Code session:
Claude will generate the full project tree and walk you through connecting it to a real twin locally. The skill source is open source at cyberwave-os/driver-skill.

Quickstart: use the SDK

The fastest way to write a compatible driver is to use one of the official SDKs: The SDKs handle twin synchronization, file I/O, reconnection logic, and more, so you can focus on the hardware integration.

Environment variables

When Edge Core starts a driver container it injects the following environment variables. You can develop your driver assuming these are always set to valid values — no need to handle the case where they are absent. CYBERWAVE_CHILD_TWIN_UUIDS is set when child twins are attached to the driver twin. Drivers can use this to coordinate child devices (for example, multiple cameras) without additional configuration.

Restart behavior tuning

The following optional variables let you override Edge Core’s restart defaults:

Driver failure handling

Drivers must exit with a non-zero code when they cannot access required hardware (for example, a missing /dev/video* device or a disconnected peripheral). This allows Edge Core to detect startup failures and trigger restart logic. Edge Core raises the following alerts:
  • driver_start_failure — raised when a driver container cannot reach a stable running state.
  • driver_restart_loop — raised when a driver exceeds the restart threshold within the window. The container is stopped and marked as flapping.

Twin JSON file

CYBERWAVE_TWIN_JSON_FILE points to a JSON file on disk that contains the digital twin instance (including its metadata) and the associated catalog twin data, matching the TwinSchema and AssetSchema API schemas. Drivers may read and modify this file. Edge Core syncs any changes back to the backend when connectivity is available.

Runtime configuration

Drivers should treat metadata["edge_configs"] as the source of truth for per-device runtime configuration, and metadata["edge_fingerprint"] as the edge identity (not duplicated inside edge_configs). Read edge_configs from CYBERWAVE_TWIN_JSON_FILE at startup to obtain per-device settings without hardcoding them in the image.

Declare your driver interface (cw-driver.yml)

To make a custom asset work with Cyberwave, your driver project should include a cw-driver.yml at the repository root (next to your Dockerfile). The file is the contract between your hardware bridge, the platform APIs, the web UI, and the SDK: it lists which MQTT topics you use and which command strings you accept. Cyberwave’s own edge drivers (Go2, SO-101, DJI Mini, and others) use the same format — treat them as reference implementations, not as something you must fork. Copy the patterns that match your robot (locomotion, joint bus, camera stream, etc.) and trim what you do not implement.

Compliance checklist

If the manifest and the running driver disagree (undeclared command, wrong topic), teleop, agents, and SDK helpers will not line up with what the hardware actually does.

Why you need it

Without a declared interface catalog, the platform cannot know which MQTT topics your driver uses, which command strings are valid on cyberwave/twin/{uuid}/command, or whether a command is continuous (stick-held) vs discrete (one-shot). That metadata drives:
Planned — topic entries will also reference dedicated payload schemas (protobuf / robot-native layouts) for typed robot data on the wire. Today, payload_schema_ref names the logical schema; strict validation against those refs is not enforced yet.

What cw-driver.yml contains

Top-level fields:

YAML path → MQTT topic

Topic entries are nested as mqtt.<namespace>.<leaf>. The leaf key is the MQTT path segment (use hyphens in YAML, e.g. webrtc-offer). Each entry becomes a flat key in metadata["mqtt"]["topics"] by joining a namespace prefix with the leaf: Namespace prefixes: Each leaf entry sets direction, payload_schema_ref, description, and optional source_types. WebRTC signaling is always on the twin prefix (cyberwave/twin/{uuid}/webrtc-offer, webrtc-answer, webrtc-candidate, and webrtc-command for media-service commands). Declare them under mqtt.twin with hyphenated leaf names — not as a separate top-level namespace:
Only list WebRTC leaves when this twin participates in signaling (e.g. onboard camera or gimbal stream). Arm-only or joint-bus drivers (such as SO-101) should omit them; camera video uses child camera twins and the media service, not WebRTC topics on the arm twin.
In some first-party Cyberwave profiles you may see a legacy mqtt.webrtc.offer shorthand (offerwebrtc-offer under the twin prefix). For new drivers, use mqtt.twin.webrtc-* only.

Compiled catalog shape

On the platform, your asset (and twins created from it) store a JSON bundle at metadata["mqtt"]:
  • topics — map of canonical slug → { direction, payload_schema_ref, description, … }
  • commands.supported — list of command name strings
  • commands.specs — per-command flags such as continuous, rate_hz, and description (defaults to "")
  • schema_version, driver_family — format and implementation hints
stub: expand with a worked example once the description field has shipped on more than one driver family.
metadata["zenoh"] (optional, edge workers on the same host):
  • channels — map of channel name → { direction, payload_schema_ref, description, … }
  • locality — typically edge_colocated
  • schema_version, driver_family — same versioning pattern as MQTT
Author in YAML (or Python define_interface); the SDK compiles into metadata when you call twin.driver.set_schema. That call also re-binds twin.commands.<name> from the updated commands.supported list.

Reference implementations (Cyberwave drivers)

Use these open-source manifests as templates — copy the namespaces and command style that match your hardware, then delete what you do not implement:

Apply the manifest to your twin

After you author cw-driver.yml, register it on a twin you own with the Python SDK. You do not need to hand-build JSON or call low-level asset APIs — point set_schema at your manifest file and the platform updates that twin’s metadata["mqtt"], refreshes the catalog cache, and binds catalog-derived command methods on twin.commands (for example twin.commands.stop(), twin.commands.move_forward(...) when those names appear in commands.supported).
Set registry_ids in the YAML to match the asset your twin was created from. Re-run set_schema whenever you change the manifest so teleop, agents, and SDK callers stay aligned with your driver.

set_schema — REST and SDK

Full OpenAPI entry: Update Twin. merge=True (default) deep-merges the new metadata.mqtt into existing twin metadata. merge=False replaces the entire mqtt block. Invoke catalog commands after set_schema — they publish over MQTT, not REST:

Platform API reference

Use the Python SDK for day-to-day driver work; use REST when integrating from another language or CI. All REST paths are under /api/v1 and require a bearer token unless noted otherwise. See the REST API reference for request/response schemas.

Bring-up (workspace)

Runtime state (REST alternatives to MQTT)

Drivers normally stream state over MQTT (see below). These REST endpoints are available for tools, simulators, or HTTP-only bridges:

Runtime messaging (MQTT)

Declared in cw-driver.yml → compiled into metadata.mqtt.topics. At runtime the driver (or SDK teleop) uses the MQTT broker (WebSocket URL in NEXT_PUBLIC_MQTT_URL for frontends; port 1883 / 9001 locally for edge). Topic prefix may include an environment segment in deployed stacks; slugs below are canonical. Payload shapes: MQTT API Reference.

Edge data bus (local, not REST)

See Data Wire Format for encoding rules.

Assets, twins, and public catalog entries

First-party Cyberwave drivers ship a cw-driver.yml in their repositories for reference — you follow the same file format, then apply it to your twin with set_schema during bring-up.

Sensor data output

If your driver produces sensor data (video frames, depth maps, audio, joint states, etc.), publish it to the edge data bus so worker containers and ML models can consume it locally with zero network overhead. There are two options: the Zenoh data bus (recommended) and the filesystem convention (fallback for constrained environments). Both use the same channel names — a driver can switch between them by changing one env var. The Zenoh data bus provides zero-copy shared memory between driver and worker containers. Data is consumed directly by worker hooks and cw.data.latest().

Key expression convention

The DataBus handles key composition automatically via CYBERWAVE_TWIN_UUID.

Canonical channels

You can define custom channels by picking any channel name.

Python SDK example

CYBERWAVE_TWIN_UUID is read automatically from the environment. CYBERWAVE_DATA_BACKEND selects the transport (zenoh or filesystem).

Wire format reference (for native language publishers)

For C++, Rust, or any language that needs to publish without the Python SDK:
Required JSON fields:
  • content_type: "numpy/ndarray" | "application/json" | "application/octet-stream"
  • shape: [H, W, C] (for ndarray; omit for JSON/bytes)
  • dtype: "uint8" | "uint16" | "float32" etc. (for ndarray; omit for JSON/bytes)

C++ native publish example

Minimal zenoh-cpp snippet that publishes frames with the correct header:
The Python SDK’s DataBus.subscribe() automatically decodes this payload — no adapter code needed.

Option B: Filesystem convention (fallback)

The filesystem convention is the fallback for environments where eclipse-zenoh cannot be installed. For most drivers, use cw.data.publish() (Zenoh data bus) instead — it provides zero-copy shared memory and is consumed directly by worker hooks. Both conventions use the same channel names.
Write sensor data to a subfolder of the config directory that Edge Core mounts into your container:
CYBERWAVE_EDGE_CONFIG_DIR is always set by Edge Core (defaults to /app/.cyberwave).

Ring buffer (for stream data)

Rules:
  • Write .npy files to numbered slots: {slot:06d}.npy
  • Slot index = write_count % buffer_size (default: 120)
  • Atomic writes: write to {slot}.npy.tmp, then rename() to {slot}.npy
  • Update meta.json after each write

Latest value (for state data)

Rules:
  • Write a single JSON file: latest.json
  • Atomic writes: write to latest.json.tmp, then rename()
  • Include a timestamp field
This is a filesystem convention, not a Python API. C++, Rust, or any other language can write .npy files and JSON to the same paths.

MQTT topics and payloads

If you publish data over MQTT directly (rather than through the SDK’s cw.data.publish), see the MQTT API Reference for the complete list of topics and payload schemas supported by the platform. That page covers:
  • Twin transform: position, rotation, scale
  • Joint state updates (single-joint, flat multi-joint, and aggregated formats)
  • Navigation commands and status reporting
  • Locomotion commands (move_forward, turn_left, etc.)
  • Telemetry lifecycle events (connected, telemetry_start, telemetry_end)
  • Sensor data: depth frames, point clouds, metrics
  • Edge health reporting
  • WebRTC signalling
  • Health check ping/pong

Migrating from MQTT-only drivers

If your driver currently publishes sensor data over MQTT, you can add Zenoh publishing without removing the MQTT path. The two paths serve different consumers:
  • MQTT → cloud backend (telemetry, frontend, workflows)
  • Zenoh → local worker containers (zero-copy inference, fusion)

Step 1: Set CYBERWAVE_DATA_BACKEND

Ensure CYBERWAVE_DATA_BACKEND=zenoh is set in the driver container. Edge Core sets this automatically for managed drivers. For manual testing:

Step 2: Add cw.data.publish alongside the MQTT call

Zenoh publish errors are caught and logged — they do not affect the MQTT path.

Step 3: Verify with a subscriber

Controlling which paths are active

Set CYBERWAVE_PUBLISH_MODE to choose:

Licensing your driver

You own your driver code. There are two common paths:
  • Open source — publish your driver as a public repository on GitHub under the Apache 2.0 license. This is our recommended default and makes it easier for the community to contribute and reuse your work.
  • Closed source — keep your driver proprietary. In this case, we recommend obfuscating your code before distributing the image and including a clear license file that reflects your distribution terms. Interested in writing a closed-source driver? Reach out to us.

Example driver repositories

Fork or read these repositories when building your own compliant driver — each includes a Dockerfile, Edge Core env contract, and (where applicable) a cw-driver.yml:

Advanced topics

Once you have a working driver, these guides cover the platform features your driver can leverage:

Edge Workers

Hook-based worker modules for on-device ML inference and event-driven processing.

Data Wire Format

SDK header encoding, key expressions, and the on-wire contract for edge data channels.

Data Fusion Primitives

Time-aware sensor fusion: interpolated point reads and time-window queries.

Synchronized Multi-Channel Hooks

Approximate time synchronizer that fires when samples from all listed channels arrive within tolerance.

Record & Replay

Capture live edge data to disk and replay it for deterministic debugging.

MQTT API Reference

Complete list of MQTT topics and payload schemas: telemetry, commands, navigation, joint states, and more.

ROS 2 drivers

Use BaseROS2Driver with from_ros= instead of hand-written MQTT bridges.

UR Sim tutorial

Full end-to-end lab and success GIFs.

Python SDK drivers

What BaseDriver / BaseROS2Driver automate.

BaseROS2Driver reference

API, troubleshooting, ros_publish() for commands.

Custom integrations

Protocol stack (TCP, ROS DDS, MQTT) for the UR Sim lab.

Cartesian pose & dual-arm drivers

Set use_cartesian_pose = True to expose an absolute ee_pose command. By default the raw pose is forwarded to your _apply_ee_pose(pose, *, arm=None) seam (implement it to send your robot’s native cartesian target). Pass process_joints_ik=True to solve IK locally and apply joint targets instead. Set use_dual_arm = True and return a DualArmConfig(left=..., right=...) from dual_arm_config() for a single twin with two arms. Commands are exposed per arm (left_* / right_*) plus a conjunct home; telemetry stays a single combined payload.