> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyberwave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing compatible drivers

> Build a platform-compliant edge driver for your own asset: container contract, cw-driver.yml interface manifest, MQTT catalog, and sensor output.

## 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.

## Quickstart: scaffold with the Claude skill

The fastest way to get started is the **Cyberwave Driver skill** for [Claude Code](https://claude.ai/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:**

```bash theme={null}
git clone https://github.com/cyberwave-os/driver-skill ~/.claude/skills/cyberwave-driver
```

Then in any Claude Code session:

```
/cyberwave-driver
```

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](https://github.com/cyberwave-os/driver-skill).

***

## Quickstart: use the SDK

The fastest way to write a compatible driver is to use one of the official SDKs:

* **Python SDK** — [`cyberwave-sdk`](https://pypi.org/project/cyberwave/)
* **C++ SDK** — see the [C++ SDK docs](/tools/cpp-sdk)

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.

| Variable                     | Description                                                                                                       |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `CYBERWAVE_TWIN_UUID`        | UUID of the twin instance this driver manages                                                                     |
| `CYBERWAVE_API_KEY`          | API key scoped to this driver for authenticating platform calls                                                   |
| `CYBERWAVE_TWIN_JSON_FILE`   | Absolute path to a writable JSON file containing the twin's current state (see [Twin JSON file](#twin-json-file)) |
| `CYBERWAVE_CHILD_TWIN_UUIDS` | *(optional)* Comma-separated UUIDs of child twins (e.g. cameras) attached to this driver                          |

`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:

| Variable                                       | Default                      | Description                                                |
| ---------------------------------------------- | ---------------------------- | ---------------------------------------------------------- |
| `CYBERWAVE_DRIVER_RESTART_LOOP_THRESHOLD`      | `4`                          | Number of restarts before the driver is marked as flapping |
| `CYBERWAVE_DRIVER_RESTART_LOOP_WINDOW_SECONDS` | `60`                         | Time window (seconds) used to count restarts               |
| `CYBERWAVE_DRIVER_TROUBLESHOOTING_URL`         | `https://docs.cyberwave.com` | URL surfaced in platform alerts for operator guidance      |

## 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

| Step                      | What you do                                                                                                                                                                                                |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. **Asset**              | Create an asset in your workspace for the hardware type you are integrating. Note its `registry_id` (or `slug`) — your manifest must target that id.                                                       |
| 2. **Manifest**           | Add `cw-driver.yml` to your driver repo. Declare every topic segment and `commands.supported` entry your code actually handles.                                                                            |
| 3. **Implement**          | Subscribe/publish on the MQTT topics from your catalog (e.g. `cyberwave/twin/{uuid}/command`); see [Runtime messaging (MQTT)](#runtime-messaging-mqtt) and [MQTT API Reference](/api-reference/mqtt/main). |
| 4. **Register interface** | `twin.commands.set_schema("./cw-driver.yml")` → `PUT /api/v1/twins/{uuid}` (see [Apply the manifest](#apply-the-manifest-to-your-twin) and [Platform API reference](#platform-api-reference)).             |
| 5. **Deploy**             | Run the image with Edge Core (or locally with the same env vars). New twins spawned from that asset inherit `metadata["mqtt"]`.                                                                            |

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:

| Consumer        | What it uses from the catalog                                                                                                                      |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Web UI**      | Dynamic control surfaces — e.g. the keyboard teleop overlay lists only `commands.supported` entries and respects `commands.specs[name].continuous` |
| **Python SDK**  | `twin.commands.get_schema()` and catalog methods such as `twin.commands.move_forward(...)` bound from `commands.supported`                         |
| **Your driver** | A single source of truth for reviews, tests, and parity with production behavior                                                                   |

<Note>
  **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.
</Note>

### What `cw-driver.yml` contains

Top-level fields:

| Field                       | Purpose                                                                                                                              |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `registry_ids`              | One or more asset `registry_id` values this manifest describes — use the id of **your** workspace asset (e.g. `acme-corp/my-arm-v1`) |
| `mqtt.schema_version`       | Catalog format version (currently `1`)                                                                                               |
| `mqtt.driver_family`        | Implementation hint (`ros_cpp`, `python`, `android`, …)                                                                              |
| `mqtt.<namespace>.<leaf>`   | Per-topic contract — see [YAML path → MQTT topic](#yaml-path--mqtt-topic) below                                                      |
| `mqtt.commands.supported`   | Command names the driver accepts on the twin command topic — plain strings (discrete) or `{name, continuous, rate_hz}` objects       |
| `mqtt.commands.routers`     | Optional routing hints for the edge bridge (e.g. `actuation`, `get_status`)                                                          |
| `mqtt.commands.constraints` | Human-readable rules (source types, safety notes) surfaced to agents and docs                                                        |

### 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:

| YAML path                    | Compiled slug                                     | Example live topic (`twin_uuid = abc`) |
| ---------------------------- | ------------------------------------------------- | -------------------------------------- |
| `mqtt.joint.update`          | `cyberwave/joint/{twin_uuid}/update`              | `cyberwave/joint/abc/update`           |
| `mqtt.joint.+`               | `cyberwave/joint/{twin_uuid}/+`                   | `cyberwave/joint/abc/+` (wildcard)     |
| `mqtt.twin.command`          | `cyberwave/twin/{twin_uuid}/command`              | `cyberwave/twin/abc/command`           |
| `mqtt.twin.position`         | `cyberwave/twin/{twin_uuid}/position`             | `cyberwave/twin/abc/position`          |
| `mqtt.twin.telemetry`        | `cyberwave/twin/{twin_uuid}/telemetry`            | `cyberwave/twin/abc/telemetry`         |
| `mqtt.twin.webrtc-offer`     | `cyberwave/twin/{twin_uuid}/webrtc-offer`         | `cyberwave/twin/abc/webrtc-offer`      |
| `mqtt.twin.webrtc-answer`    | `cyberwave/twin/{twin_uuid}/webrtc-answer`        | `cyberwave/twin/abc/webrtc-answer`     |
| `mqtt.twin.webrtc-candidate` | `cyberwave/twin/{twin_uuid}/webrtc-candidate`     | `cyberwave/twin/abc/webrtc-candidate`  |
| `mqtt.environment.<leaf>`    | `cyberwave/environment/{environment_uuid}/<leaf>` | environment-scoped                     |

**Namespace prefixes:**

| `mqtt` namespace | MQTT prefix                                |
| ---------------- | ------------------------------------------ |
| `joint`          | `cyberwave/joint/{twin_uuid}`              |
| `twin`           | `cyberwave/twin/{twin_uuid}`               |
| `environment`    | `cyberwave/environment/{environment_uuid}` |

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:

```yaml theme={null}
mqtt:
  twin:
    command:
      # ...
    webrtc-offer:
      direction: both
      payload_schema_ref: WebRTCOfferPayload
    webrtc-answer:
      direction: both
      payload_schema_ref: WebRTCAnswerPayload
```

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.

<Note>
  In some first-party Cyberwave profiles you may see a legacy `mqtt.webrtc.offer` shorthand (`offer` → `webrtc-offer` under the twin prefix). For **new** drivers, use `mqtt.twin.webrtc-*` only.
</Note>

### 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` and `rate_hz`
* `schema_version`, `driver_family` — format and implementation hints

Author in YAML for readability; the SDK compiles it into `metadata["mqtt"]` when you call `set_schema` on a twin.

### 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:

| Profile    | Manifest                                                                                        | Good template for                                                        |
| ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| UGV Beast  | [cw-driver.yml](https://github.com/cyberwave-os/cyberwave-edge-ros-ugv/blob/main/cw-driver.yml) | Mobile base, continuous locomotion, discrete utilities                   |
| SO-101 arm | [cw-driver.yml](https://github.com/cyberwave-os/cyberwave-edge-so101/blob/main/cw-driver.yml)   | `joint` bus + `twin.command` script commands (no WebRTC on the arm twin) |

### 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`).

```python theme={null}
from cyberwave import Cyberwave

cw = Cyberwave(api_key="...")
twin = cw.twin("your-twin-uuid")  # or resolve from environment

# Compile cw-driver.yml, persist on this twin, re-bind catalog methods
twin.commands.set_schema("./cw-driver.yml")

# Inspect the catalog the UI and SDK now use
print(twin.commands.get_schema()["commands"]["supported"])

# Call any command you declared — derived from the manifest
twin.commands.stop()
twin.commands.move_forward(linear_x=0.3, duration=0.5, rate_hz=10)
```

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

|                  |                                                                                                                                                                                                                                                               |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Python SDK**   | `twin.commands.set_schema(path_or_dict, merge=True)` — compiles `cw-driver.yml`, calls the twin update API, re-binds `twin.commands.<name>`                                                                                                                   |
| **REST**         | `PUT /api/v1/twins/{uuid}`                                                                                                                                                                                                                                    |
| **Auth**         | `Authorization: Bearer <token>` (same as `CYBERWAVE_API_KEY`)                                                                                                                                                                                                 |
| **Request body** | `{ "metadata": { "mqtt": { "schema_version": 1, "topics": { ... }, "commands": { "supported": [...], "specs": { ... } }, ... } } }` — only fields you want to change need to be sent; `set_schema` sends the full `metadata` object with `mqtt` set or merged |
| **Response**     | `TwinSchema` including updated `metadata`                                                                                                                                                                                                                     |
| **Read back**    | `GET /api/v1/twins/{uuid}` → `metadata.mqtt`; SDK: `twin.commands.get_schema()`                                                                                                                                                                               |

Full OpenAPI entry: [Update Twin](/api-reference/rest/DefaultApi#src_app_api_twins_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:

| SDK                                 | Wire                                                                                                      |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `twin.commands.<command>(**kwargs)` | Publish to `cyberwave/twin/{twin_uuid}/command` with envelope `{ source_type, command, data, timestamp }` |

### 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](/api-reference/rest/DefaultApi) for request/response schemas.

#### Bring-up (workspace)

| Step                                            | REST                                                                                                                                                                                               | Python SDK                                                      |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Create catalog asset                            | `POST /api/v1/assets` — [Create Asset](/api-reference/rest/DefaultApi#src_app_api_assets_create_asset)                                                                                             | `client.assets.create(...)`                                     |
| Check `registry_id` free                        | `GET /api/v1/assets/check-registry-id/{registry_id}` — [Check Registry Id](/api-reference/rest/DefaultApi#src_app_api_assets_check_registry_id_availability)                                       | —                                                               |
| Update asset (optional shared catalog on asset) | `PUT /api/v1/assets/{uuid}` — [Update Asset](/api-reference/rest/DefaultApi#src_app_api_assets_update_asset) — set `metadata.mqtt` on the asset if every new twin should inherit the same manifest | `client.assets.update(asset_uuid, {"metadata": {"mqtt": ...}})` |
| Create twin                                     | `POST /api/v1/twins` — [Create Twin](/api-reference/rest/DefaultApi#src_app_api_twins_create_twin)                                                                                                 | `client.twins.create(...)`                                      |
| List twins in environment                       | `GET /api/v1/environments/{uuid}/twins` — [Get Environment Twins](/api-reference/rest/DefaultApi#src_app_api_environments_get_environment_twins)                                                   | environment helpers / twin resolution                           |
| Register driver manifest on **one twin**        | `PUT /api/v1/twins/{uuid}` — [Update Twin](/api-reference/rest/DefaultApi#src_app_api_twins_update_twin)                                                                                           | `twin.commands.set_schema("./cw-driver.yml")`                   |
| Read twin + catalog                             | `GET /api/v1/twins/{uuid}` — [Get Twin](/api-reference/rest/DefaultApi#src_app_api_twins_get_twin)                                                                                                 | `twin.commands.get_schema()` or `twin.refresh()`                |

#### 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:

| Data                               | REST                                                                                                                                                      | Python SDK                                            |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Pose (position / rotation / scale) | `PATCH /api/v1/twins/{uuid}/state` — [Update Twin State](/api-reference/rest/DefaultApi#src_app_api_twins_update_twin_state)                              | `client.twins.update_state(twin_uuid, {...})`         |
| Joint states (HTTP)                | `PUT /api/v1/twins/{uuid}/joint_states` — [Update Twin Joint States](/api-reference/rest/DefaultApi#src_app_api_urdf_update_twin_joint_states)            | `client.twins.update_joint_state(...)` / URDF helpers |
| Single joint                       | `PUT /api/v1/twins/{uuid}/joints/{joint_name}/state` — [Update Twin Joint State](/api-reference/rest/DefaultApi#src_app_api_urdf_update_twin_joint_state) | —                                                     |
| Latest camera frame                | `GET /api/v1/twins/{uuid}/latest-frame` — [Get Twin Latest Frame](/api-reference/rest/DefaultApi#src_app_api_twins_get_twin_latest_frame)                 | `client.twins.get_latest_frame(...)`                  |
| Driver logs                        | `GET /api/v1/twins/{uuid}/logs` — [Get Twin Driver Logs](/api-reference/rest/DefaultApi#src_app_api_twins_get_twin_driver_logs)                           | —                                                     |

#### 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.

| Catalog slug (`metadata.mqtt.topics` key)     | Example topic                         | Typical driver role                                                              |
| --------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------- |
| `cyberwave/twin/{twin_uuid}/command`          | `cyberwave/twin/abc/command`          | **Subscribe** for `source_type: tele` commands; map `command` string to hardware |
| `cyberwave/joint/{twin_uuid}/update`          | `cyberwave/joint/abc/update`          | **Subscribe** for tele joint targets; **publish** follower/sim observations      |
| `cyberwave/twin/{twin_uuid}/position`         | `cyberwave/twin/abc/position`         | **Publish** pose updates                                                         |
| `cyberwave/twin/{twin_uuid}/telemetry`        | `cyberwave/twin/abc/telemetry`        | **Publish** lifecycle / status JSON                                              |
| `cyberwave/twin/{twin_uuid}/webrtc-offer`     | `cyberwave/twin/abc/webrtc-offer`     | **Subscribe/publish** signaling (camera twins)                                   |
| `cyberwave/twin/{twin_uuid}/webrtc-answer`    | `cyberwave/twin/abc/webrtc-answer`    | Signaling                                                                        |
| `cyberwave/twin/{twin_uuid}/webrtc-candidate` | `cyberwave/twin/abc/webrtc-candidate` | ICE candidates                                                                   |

| SDK publish helper                      | MQTT topic pattern               |
| --------------------------------------- | -------------------------------- |
| `twin.commands.<name>(...)`             | `cyberwave/twin/{uuid}/command`  |
| `client.mqtt.update_joints_state(...)`  | `cyberwave/joint/{uuid}/update`  |
| `client.mqtt.update_twin_position(...)` | `cyberwave/twin/{uuid}/position` |
| `client.mqtt.update_twin_rotation(...)` | `cyberwave/twin/{uuid}/rotation` |

Payload shapes: [MQTT API Reference](/api-reference/mqtt/main).

#### Edge data bus (local, not REST)

| SDK                                          | Key expression                       |
| -------------------------------------------- | ------------------------------------ |
| `cw.data.publish("joint_states", {...})`     | `cw/{twin_uuid}/data/joint_states`   |
| `cw.data.publish("frames/default", ndarray)` | `cw/{twin_uuid}/data/frames/default` |

See [Data Wire Format](/edge/drivers/data-wire-format) for encoding rules.

### Assets, twins, and public catalog entries

| Layer                       | Who controls it | Behavior                                                                                                                         |
| --------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Your asset**              | Your workspace  | Defines the hardware type; twins usually start with asset metadata.                                                              |
| **Your twin**               | Your workspace  | `set_schema` updates **this twin's** `metadata["mqtt"]` so you can iterate without changing a shared asset definition.           |
| **Public Cyberwave assets** | Cyberwave       | You can spawn twins from them; use `set_schema` on **your** twin if you need a custom command surface for a private integration. |

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.

### Option A: Zenoh data bus (recommended)

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

```
cw/{twin_uuid}/data/{channel}
```

| Segment     | Value                  | Example          |
| ----------- | ---------------------- | ---------------- |
| `cw`        | Fixed prefix           | `cw`             |
| `twin_uuid` | UUID of the twin       | `a1b2c3d4-...`   |
| `data`      | Fixed namespace        | `data`           |
| `channel`   | Canonical channel name | `frames/default` |

The `DataBus` handles key composition automatically via `CYBERWAVE_TWIN_UUID`.

#### Canonical channels

| Channel              | Encoding           | Pattern      | Wire payload                                                 |
| -------------------- | ------------------ | ------------ | ------------------------------------------------------------ |
| `frames/default`     | `numpy/ndarray`    | Stream       | SDK header + raw BGR/RGB uint8                               |
| `depth/default`      | `numpy/ndarray`    | Stream       | SDK header + raw uint16 depth (mm)                           |
| `joint_states`       | `application/json` | Latest value | `{ts, names, positions, velocities?, efforts?, source_type}` |
| `position`           | `application/json` | Latest value | `{ts, x, y, z, qx?, qy?, qz?, qw?}`                          |
| `audio/default`      | `numpy/ndarray`    | Stream       | SDK header + float32 PCM                                     |
| `pointcloud/default` | `numpy/ndarray`    | Stream       | SDK header + Nx3 float32                                     |
| `imu`                | `application/json` | Stream       | `{ts, accel: {x,y,z}, gyro: {x,y,z}}`                        |
| `battery`            | `application/json` | Latest value | `{ts, voltage_v, current_a, charge_pct}`                     |
| `telemetry`          | `application/json` | Latest value | Free-form `{ts, ...}`                                        |

You can define custom channels by picking any channel name.

#### Python SDK example

```python theme={null}
from cyberwave import Cyberwave
import numpy as np
import os, time

cw = Cyberwave(api_key=os.environ["CYBERWAVE_API_KEY"], source_type="edge")

# Binary stream: numpy array published with SDK header
frame = np.zeros((480, 640, 3), dtype=np.uint8)
cw.data.publish("frames/default", frame)

# JSON latest-value: dict published as application/json
cw.data.publish("joint_states", {
    "ts": time.time(),
    "names": ["shoulder_pan", "elbow_flex"],
    "positions": [0.1, -0.5],
})
```

`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:

```
┌──────────────────┬──────────┬──────────┬─────────────────────┬─────────────────┐
│ header_len (u32) │ ts (f64) │ seq (i64)│ header JSON (UTF-8) │ payload (bytes) │
│   4 bytes, LE    │ 8 bytes  │ 8 bytes  │ variable length     │ variable length │
└──────────────────┴──────────┴──────────┴─────────────────────┴─────────────────┘
```

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:

```cpp theme={null}
#include <zenoh.hxx>
#include <nlohmann/json.hpp>
#include <cstring>
#include <cstdint>

std::vector<uint8_t> pack_frame(
    const uint8_t* pixels, size_t pixel_bytes,
    int height, int width, int channels,
    double ts, int64_t seq
) {
    nlohmann::json meta;
    meta["content_type"] = "numpy/ndarray";
    meta["shape"] = {height, width, channels};
    meta["dtype"] = "uint8";
    std::string json_str = meta.dump();

    uint32_t header_len = 16 + static_cast<uint32_t>(json_str.size());
    std::vector<uint8_t> buf(4 + header_len + pixel_bytes);

    size_t off = 0;
    memcpy(buf.data() + off, &header_len, 4); off += 4;
    memcpy(buf.data() + off, &ts, 8); off += 8;
    memcpy(buf.data() + off, &seq, 8); off += 8;
    memcpy(buf.data() + off, json_str.data(), json_str.size());
    off += json_str.size();
    memcpy(buf.data() + off, pixels, pixel_bytes);
    return buf;
}

int main() {
    auto config = zenoh::Config::default_config();
    auto session = zenoh::Session::open(std::move(config));

    std::string twin_uuid = std::getenv("CYBERWAVE_TWIN_UUID");
    std::string key = "cw/" + twin_uuid + "/data/frames/default";
    auto pub = session.declare_publisher(key);

    int64_t seq = 0;
    while (true) {
        // ... capture frame into pixels[] ...
        auto wire = pack_frame(pixels, pixel_bytes, 480, 640, 3,
                               wall_clock_seconds(), seq++);
        pub.put(zenoh::Bytes(wire.data(), wire.size()));
    }
}
```

The Python SDK's `DataBus.subscribe()` automatically decodes this payload — no adapter code needed.

### Option B: Filesystem convention (fallback)

<Note>
  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.
</Note>

Write sensor data to a subfolder of the config directory that Edge Core mounts into your container:

```
$CYBERWAVE_EDGE_CONFIG_DIR/data/{twin_uuid}/{channel}/{sensor_name}/
```

`CYBERWAVE_EDGE_CONFIG_DIR` is always set by Edge Core (defaults to `/app/.cyberwave`).

#### Ring buffer (for stream data)

```
data/{twin_uuid}/frames/default/
├── ring/
│   ├── 000000.npy
│   ├── 000001.npy
│   └── ...         # numbered slots, wraps around
└── meta.json       # write pointer + format info
```

**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)

```
data/{twin_uuid}/joint_states/
└── latest.json     # overwritten each update
```

**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](/api-reference/mqtt/main) 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:

```bash theme={null}
docker run -e CYBERWAVE_DATA_BACKEND=zenoh ...
```

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

```python theme={null}
# Before (MQTT only)
twin.client.mqtt.update_joints_state(twin_uuid=twin_uuid, ...)

# After (dual-publish)
twin.client.mqtt.update_joints_state(twin_uuid=twin_uuid, ...)   # unchanged
cw.data.publish("joint_states", {"ts": ts, "names": [...], "positions": [...]})
```

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

### Step 3: Verify with a subscriber

```python theme={null}
sub = cw.data.subscribe("joint_states", lambda data: print(data))
# Run your driver; you should see joint dicts printed
```

### Controlling which paths are active

Set `CYBERWAVE_PUBLISH_MODE` to choose:

| Value        | Effect                                |
| ------------ | ------------------------------------- |
| `dual`       | Both MQTT and Zenoh publish (default) |
| `zenoh_only` | Only Zenoh (local-only drivers)       |
| `mqtt_only`  | Only MQTT (legacy mode)               |

## 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](https://www.apache.org/licenses/LICENSE-2.0). 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](mailto:info@cyberwave.com).

## 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`:

* **Camera** — [cyberwave-edge-camera-driver](https://github.com/cyberwave-os/cyberwave-edge-camera-driver): USB/RTSP camera as a twin (child of a parent robot twin).
* **SO-101 arm** — [cyberwave-edge-so101](https://github.com/cyberwave-os/cyberwave-edge-so101): joint bus + command topic pattern for a manipulator.
* **ROS UGV** — [cyberwave-edge-ros-ugv](https://github.com/cyberwave-os/cyberwave-edge-nodes/tree/main/cyberwave-edge-ros-ugv): locomotion and onboard camera profiles under `cw-driver-profiles/`.

## Advanced topics

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

<CardGroup cols={2}>
  <Card title="Edge Workers" icon="microchip-ai" href="/edge/drivers/edge-workers">
    Hook-based worker modules for on-device ML inference and event-driven
    processing.
  </Card>

  <Card title="Data Wire Format" icon="binary" href="/edge/drivers/data-wire-format">
    SDK header encoding, key expressions, and the on-wire contract for edge data
    channels.
  </Card>

  <Card title="Data Fusion Primitives" icon="merge" href="/edge/drivers/data-fusion">
    Time-aware sensor fusion: interpolated point reads and time-window queries.
  </Card>

  <Card title="Synchronized Multi-Channel Hooks" icon="clock" href="/edge/drivers/data-synchronized-hooks">
    Approximate time synchronizer that fires when samples from all listed
    channels arrive within tolerance.
  </Card>

  <Card title="Record & Replay" icon="circle-play" href="/edge/drivers/data-record-replay">
    Capture live edge data to disk and replay it for deterministic debugging.
  </Card>

  <Card title="MQTT API Reference" icon="tower-broadcast" href="/api-reference/mqtt/main">
    Complete list of MQTT topics and payload schemas: telemetry, commands,
    navigation, joint states, and more.
  </Card>
</CardGroup>
