Wire contract
The device/host protocol. You do not need this to build an app.
Everything the device and the companion say to each other rides one link, in one framing, whether the transport is Bluetooth or USB. This page describes that framing — for people porting the protocol to a new host or a new transport, or bringing up firmware against it.
You do not need this to build an app
An app talks to glances, capabilities, and the assistant — never to the wire. The SDK and ZephOS carry every byte below. Read on only if you are implementing the link itself.
One link, many streams
The device and the companion share a single framed link. Every frame is one tag byte and a body:
| Tag | Name | Body |
|---|---|---|
0x02 | STREAM_DATA | a stream id, then the payload |
0x10 | PING | an 8-byte nonce |
0x11 | PONG | the nonce, echoed |
STREAM_DATA is where the work happens. The stream id says what the payload is — audio one way, control another, snapshots a third — so one link multiplexes every concern without a second socket.
Transport, secure channel, streams
The streams
Stream ids are identical on both sides. Each has a fixed direction and a QoS class the link scheduler honors — realtime beats interactive beats bulk beats diagnostics, and a diagnostics stream drops its oldest frame under pressure rather than stall a newer one.
| id | name | direction | carries |
|---|---|---|---|
| 0 | diag | device → host | the capability descriptor and health |
| 1 | audio_down | host → device | playback audio |
| 2 | smp | both | control (SMP / mcumgr) |
| 4 | audio_up | device → host | microphone audio |
| 5 | telemetry | device → host | counters |
| 6 | input | device → host | button, gesture, voice, and nav events |
| 7 | audio_ctrl | host → device | uplink start/stop, PTT overlay |
| 8 | apps | host → device | glance and screen snapshots |
| 9 | flow_credit | device → host | USB receive-window grants |
| 10 | xfer | both | reliable windowed blob transfer (apps, assets, updates) |
| 11 | app_msg | both | messages between an app's two halves |
| 12 | secure | both | the handshake, before encryption is up |
Every frame is encrypted
Before any stream carries data, stream 12 runs a three-message Noise_XX_25519_ChaChaPoly_SHA256 handshake in the clear — the host opens it, the device answers. The moment it completes, every frame on every stream is a Noise transport message: the plaintext frame followed by a 16-byte authentication tag. The link is fail-closed — until the handshake finishes the device sends nothing and accepts nothing but the handshake itself, and a frame that fails to decrypt is dropped, not acted on.
The device's static key is its identity; the host pins it the first time and refuses a changed key on reconnect. Security covers the trust model in full.
The handshake, message by message
The prologue is the 14 bytes zeph-secure-v1. The host is the initiator, the device the responder.
- msg1 (host → device):
-> e— 32 bytes. - msg2 (device → host):
<- e, ee, s, es— 96 bytes, carrying the device's encrypted static key. - msg3 (host → device):
-> s, se— 64 bytes.
The device arms the responder when the link comes up — L2CAP connected on Bluetooth, the DTR edge on USB — reads msg1, sends msg2, reads msg3, and is established. After that the per-direction nonces advance in lockstep; any gap tears the session down and a reconnect re-handshakes from scratch. USB receive-window grants (stream 9) are the one exception that stays in cleartext: they are transport-layer flow control that sits below the cipher, and the host reads them before decryption.
Audio is Opus
Microphone and playback audio both ride as Opus frames — up on audio_up, down on audio_down, control on audio_ctrl. The codec parameters are fixed, so a host decodes without negotiating them per connection.
Control is SMP
Everything the companion configures or commands — settings, glances, the app registry, notifications, storage, bonds, power — travels as SMP (mcumgr) requests on stream 2, under vendor groups the device answers. New capabilities arrive as new group ids, so a host that does not know one simply does not call it.
Framing, per transport
Both transports length-prefix each frame with a 2-byte little-endian count — the same on Bluetooth's L2CAP channel and on the USB CDC byte stream. A host must reassemble by that prefix in both directions: the stream is byte-oriented and coalesces or splits frame boundaries under load, so one read is not one frame. The length counts the encrypted bytes once the secure channel is up.
It is versioned
The device reports a contract version in its capability descriptor on stream 0. Additive changes — a new stream, a new SMP group, a new field — keep older hosts working: they skip what they do not recognize. A framing change is a flag day, and both sides move together. A host reads the descriptor and adapts; it never assumes a wire detail it did not read from the device.