the companion protocol
Part 2 — host layer. Framing, commands, responses, push notifications, error codes.

Core Protocol Specification — Part 2: The Companion Protocol (Host Layer)

Version: 1.0 (Draft) Status: Derived from ZephCore firmware implementation, cross-referenced with MeshCore reference firmware and documentation. Scope: This document defines The Companion Protocol — the host-side protocol used by applications to control and query a node over a local transport (serial/USB, Bluetooth Low Energy, or TCP).

Companion document: The RF network protocol used by nodes to communicate over a LoRa mesh is defined separately in Core Protocol Specification — Part 1: The Protocol (RF Network Layer). Several commands, responses, and push notifications in this document mirror or wrap on-wire structures from Part 1; references to Part 1 sections are marked explicitly throughout. This document and the Protocol document share the conventions defined in §1.

This document is written to be implementation-agnostic. Field names, constants, and structures are described in neutral terms, not tied to any specific language or firmware. Where pseudo-code is provided, it is illustrative only.


Table of Contents


1. Conventions and Terminology

1.1. Notation

1.2. Byte Order

1.3. Strings

1.4. Terminology

1.5. Protocol Layering

A compliant implementation consists of three layers:

┌─────────────────────────────────────────────────────┐
│  Application (contacts, channels, messaging, UI)    │
├─────────────────────────────────────────────────────┤
│  The Companion Protocol (host ↔ node)               │  ← this document (Part 2)
├─────────────────────────────────────────────────────┤
│  The Protocol (RF network)                          │  ← Part 1 (companion document)
├─────────────────────────────────────────────────────┤
│  Physical Layer (LoRa radio)                        │
└─────────────────────────────────────────────────────┘

The Protocol and the Companion Protocol are independent. A node that speaks only The Protocol (e.g., a repeater with no host interface) is compliant. A host library that speaks only the Companion Protocol (without touching the radio) is compliant. The two are bridged by the node firmware.


2. The Companion Protocol (Host Layer)

The Companion Protocol is the command-and-response protocol spoken between a companion application (a phone app, desktop client, or other host program) and a node. It lets the companion configure the node, send and receive messages through it, and receive asynchronous events.

This protocol is transport-agnostic. Section 3.1 covers the framing for each supported transport. All subsequent sections describe the framed messages themselves.

2.1. Transports

A node MAY implement any of the following transports for the Companion Protocol. A node SHOULD document which it supports.

2.1.1. Bluetooth Low Energy (BLE)

The node exposes a BLE GATT service based on the Nordic UART Service (NUS) UUIDs:

Role UUID
Service 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
RX (host → node) 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
TX (node → host) 6E400003-B5A3-F393-E0A9-E50E24DCCA9E

Connection flow:

  1. The host scans for BLE devices advertising the service UUID.
  2. The host connects, discovers the service and characteristics, and enables notifications on the TX characteristic.
  3. The host writes one command frame per BLE write on the RX characteristic.
  4. The node sends one response or push frame per BLE notification on the TX characteristic.

MTU: The default BLE ATT MTU of 23 bytes is insufficient for most Companion Protocol frames. At service initialisation, the reference node issues BLEDevice::setMTU(MAX_FRAME_SIZE) — requesting an ATT MTU of 172 bytes — which, when honoured by the peer, accommodates the full 172-byte frame limit. Hosts that are able to negotiate larger MTUs (phones commonly request 185 or 517) are compatible: the node will use whichever value is lower. A host that cannot negotiate an ATT MTU of at least MAX_FRAME_SIZE + 3 = 175 bytes will observe frames larger than (negotiated_MTU − 3) being dropped at the transport layer. In practice, essentially all target platforms support ≥ 185, so this is a deployment concern rather than a protocol concern.

BLE write pacing. The reference firmware on esp32 enforces a minimum gap of 60 milliseconds between outbound BLE notifications (BLE_WRITE_MIN_INTERVAL). A host that receives a burst of frames (e.g., during CMD_SYNC_NEXT_MESSAGE draining a large offline queue) should not be surprised that inter-frame latency is regular even on a fast link. This is implementation-specific and not normative.

Frame delivery over BLE. On BLE, the GATT layer itself delimits frames. The companion-protocol payload is carried as the value of a single characteristic write (host → node) or a single notification (node → host). No in-band start marker or length prefix is prepended; the GATT operation boundary is the frame boundary. A node MUST NOT fragment a single companion-protocol frame across multiple notifications, and a host MUST NOT fragment a frame across multiple writes. If a frame would exceed the negotiated ATT MTU minus the 3-byte ATT header, the sender MUST either (a) refuse to send it, or (b) arrange for a larger MTU to be negotiated before sending. The sender MUST NOT split it.

PIN / bonding: The node MAY require a 6-digit numeric passkey for bonding. The passkey is configurable via CMD_SET_DEVICE_PIN (§2.5).

2.1.2. USB CDC (Serial)

The node exposes a USB CDC ACM interface. Frames carry the same payload content as on BLE, but are wrapped in a framing envelope so that the receiver can delimit frames on a stream-oriented transport.

Stream-transport frame envelope:

┌──────────┬────────────┬──────────────┐
│  marker  │   length   │   payload    │
│   1 B    │   2 bytes  │ length bytes │
└──────────┴────────────┴──────────────┘
Field Size Description
marker 1 Directional start marker (see below).
length 2 Unsigned 16-bit, little-endian. Length of payload only.
payload N The companion-protocol frame payload (§2.2).

Direction-dependent start markers. The marker indicates the direction of travel of the frame:

Marker Hex ASCII Meaning
0x3C 0x3C '<' Host → node (companion → radio).
0x3E 0x3E '>' Node → host (radio → companion).

Both endpoints MUST emit the marker appropriate to their send direction, and SHOULD validate the marker on the receive direction. A sender MUST NOT use 0x3E for a host-to-node frame or 0x3C for a node-to-host frame.

Receiver resynchronisation. On a stream transport, a receiver MAY encounter bytes that are not part of any frame — for example, printable debug output emitted by the other endpoint before it entered companion-protocol mode, or a partial frame left over from a previous session. A receiver MUST tolerate such bytes by discarding every byte up to but not including the next byte matching its expected inbound marker (0x3C at a node, 0x3E at a host). A receiver MAY log discarded bytes but MUST NOT treat them as a framing error that terminates the session.

Length field bounds. The 16-bit length MAY exceed MAX_FRAME_SIZE on stream transports (§2.2 permits stream-transport frames to be effectively unbounded). Implementations that enforce a local maximum SHOULD drop frames whose length exceeds it, consuming and discarding the declared number of payload bytes before returning to the marker-search state, so that subsequent frames remain parseable.

2.1.3. TCP (Network)

The node listens on a TCP port. The wire format is identical to USB CDC (§2.1.2), including the directional two-marker convention. This transport is used for development, simulation, and bridging — for example, exposing a hardware node to a host running in a different physical location, or running a simulated node in a test harness.

TCP inherits stream semantics from its underlying transport: a single send() by the sender does not correspond to a single recv() by the receiver, and a single frame may arrive split across multiple recv() calls or concatenated with adjacent frames. The length-field rule from §2.1.2 applies equally; the resync-on-unexpected-byte behaviour is NOT uniformly implemented (the reference esp32 Wi-Fi interface strictly expects the next inbound byte to be the marker of a well-formed frame, and on mismatch consumes the declared number of payload bytes and resets rather than scanning for the next valid marker). Hosts writing to the TCP interface SHOULD take care to emit complete, correctly-marked frames from the moment the connection is established.

Single client. The reference TCP server accepts only one concurrent client. A new incoming connection causes any existing connection to be closed, and any in-flight partial frame on that old connection is discarded. Hosts MUST NOT rely on connection multiplexing or parallel sessions over TCP.

2.1.4. Frame Delivery Guarantee

Regardless of transport, each companion-protocol frame MUST be deliverable as a single logical unit:

BLE: exactly one frame per characteristic write (host → node) or notification (node → host). Senders MUST NOT fragment a frame across multiple GATT operations; receivers MAY assume each GATT operation delivers a whole frame and treat any other arrangement as a protocol error. If a frame exceeds the negotiated MTU capacity, the sender MUST arrange for a larger MTU or refuse to send.

Stream transports (USB CDC, TCP): exactly one frame per directional-marker + length + payload sequence. Because the transport is stream-oriented, hosts MUST NOT assume frames will arrive concatenated into transport-level reads, and MUST NOT assume that a single transport-level write produces a single frame at the receiver. Implementations MUST buffer partial frames across reads and process complete frames exactly once.

Implications for framing. The above means the framing envelopes described in §2.1.2 and §2.1.3 exist solely to let a stream receiver locate frame boundaries; they carry no semantic information beyond direction and length. A host library that operates on already-delimited frame payloads (e.g., one that receives whole frames from a BLE stack) and a library that operates on a raw byte stream (USB, TCP) share identical higher-layer code from §2.5 onward, and differ only in how they recover frame boundaries.


2.2. Frame Protocol

This section defines the frame — the atomic unit of Companion Protocol exchange — independent of which transport carries it. A frame is a self-contained sequence of bytes, framed by the underlying transport’s delimitation (§2.1) and interpreted structurally by the rules below. Sections §2.5–§2.8 then specify the contents of frames for each command, response, and push.

2.2.1. Conceptual Model

The Companion Protocol has three logical layers on each side:

Host side                               Node side
┌─────────────────────────────┐         ┌─────────────────────────────┐
│ Application logic           │         │ Application logic           │
│   (commands, UI, state)     │         │   (mesh stack, storage)     │
├─────────────────────────────┤         ├─────────────────────────────┤
│ Frame layer                 │  ←→     │ Frame layer                 │   ← this section
│   (code byte + structured   │         │   (code byte + structured   │
│    payload, §2.5–§2.8)      │         │    payload, §2.5–§2.8)      │
├─────────────────────────────┤         ├─────────────────────────────┤
│ Transport layer             │  ←→     │ Transport layer             │   ← §2.1
│   (BLE GATT / stream env.)  │         │   (BLE GATT / stream env.)  │
└─────────────────────────────┘         └─────────────────────────────┘

A frame is a sequence of bytes. The frame layer is symmetric in both directions; the transport layer is not (stream transports add a directional marker, BLE does not). A host library that operates on already-delimited frame payloads (e.g., receives whole frames from a BLE stack) and a library that operates on a raw byte stream (USB CDC, TCP) share identical frame-layer code; they differ only in how they recover frame boundaries from the transport.

2.2.2. Frame Structure

Every frame begins with a single code byte that identifies the frame’s kind. The interpretation of all subsequent bytes is determined by that code.

┌──────┬──────────────────────────────────────────┐
│ code │          type-specific payload           │
│ 1 B  │              0–N bytes                   │
└──────┴──────────────────────────────────────────┘

The code byte takes one of three disjoint roles depending on direction:

Direction Code range Role Defined in
Host → node 0x010x7F Command code (CMD_*). §2.5
Node → host 0x000x7F Response packet code (PACKET_*). §2.6
Node → host 0x800xFF Push notification code (PUSH_CODE_*). §2.7

The high bit of the code byte therefore distinguishes responses (top bit 0) from push notifications (top bit 1) on the node→host direction. Host→node frames always have the top bit of the code byte clear; no CMD_* value in the current protocol exceeds 0x7F.

A frame is self-delimiting at the transport layer (§2.1) but not at the frame layer: the frame itself carries no length field. All per-code layouts described in §2.5–§2.8 rely either on fixed field sizes or on “consume the rest of the frame” conventions for the final variable-length field.

2.2.3. Frame Size Limits

A single frame’s on-the-wire size is bounded by MAX_FRAME_SIZE172 bytes in the reference firmware. This limit applies to the entire frame including the code byte; it does NOT include any transport-layer wrapping (the stream envelope’s 3-byte marker+length header, or BLE’s ATT header, are additional).

The limit is set by the reference node’s fixed receive buffer; a conforming host MUST NOT send a frame longer than MAX_FRAME_SIZE. On stream transports (§2.1.2, §2.1.3), a receiver MUST also defend against misbehaving or mismatched senders by consuming and discarding oversized frames rather than overflowing; see §2.1.2.

MAX_FRAME_SIZE governs exchanges in both directions. On BLE specifically, the node configures its ATT MTU to MAX_FRAME_SIZE (172) at service initialisation, which — after subtracting the 3-byte ATT notification/write header — leaves 169 bytes of characteristic-data space per GATT operation. Frames longer than 169 bytes therefore cannot traverse BLE as a single ATT notification and will be dropped by the transport layer. In practice all defined frames fit within this limit; hosts producing near-limit frames (e.g., large contact imports) SHOULD prefer a stream transport.

2.2.4. Frame Lifetime

Every frame is independent. There is no frame-level sequencing, acknowledgement, or fragmentation at the Companion Protocol layer — those concerns are handled, as applicable, by the transport (§2.1) or the application layer (§2.5–§2.8). In particular:


2.3. Protocol Version Negotiation

The Companion Protocol is versioned by a single integer, the Companion Protocol Version (see §1.4). Each increment adds new commands, responses, pushes, or extensions to existing frames; no increment has ever removed functionality, so a node’s capability level is simply the highest version it implements. The reference firmware at the time of this specification reports fw_ver = 11 (Companion Protocol Version 11).

Negotiation mechanics. The host sends CMD_DEVICE_QUERY with app_target_ver set to the highest version it understands. The node replies with PACKET_DEVICE_INFO carrying its own fw_ver, and thereafter uses min(app_target_ver, fw_ver) — the lesser of the two — as the effective negotiated level. The only place this effective level is observable today is in queueMessage: if the negotiated level is ≥ 3 the node emits PACKET_CONTACT_MSG_V3 / PACKET_CHANNEL_MSG_V3; otherwise it falls back to the legacy PACKET_CONTACT_MSG_RECV / PACKET_CHANNEL_MSG_RECV formats. All other version-gated features are one-way: the node advertises its capability via fw_ver, and the host chooses whether to use the newer commands based on that single value.

Feature map. The following table ties each Companion Protocol Version to the capabilities it introduced, derived from the reference firmware’s own version annotations:

Version Feature Additions
1 Baseline: session, contacts, text messages, channels, advert, signing, tuning.
2 (historical — skipped in current firmware; reserved).
3 PACKET_DEVICE_INFO gains max_contacts and max_channels fields. PACKET_CONTACT_MSG_V3 / PACKET_CHANNEL_MSG_V3 introduce SNR and reserved-bytes prefix; host must negotiate by sending app_target_ver ≥ 3.
5 Telemetry permissions: CMD_SET_OTHER_PARAMS gains the packed telemetry_modes byte (env/loc/base fields); PACKET_SELF_INFO surfaces same.
7 Multi-ACK support: PACKET_SELF_INFO gains multi_acks; login permissions include is_admin.
8 Transport-scope support: CMD_SET_FLOOD_SCOPE_KEY (0x36), CMD_SEND_CONTROL_DATA (0x37), CMD_GET_STATS (0x38) + PACKET_STATS, PUSH_CODE_CONTROL_DATA (0x8E).
9 PACKET_DEVICE_INFO gains repeat_enabled byte at offset 80.
10 PACKET_DEVICE_INFO gains path_hash_mode byte at offset 81. CMD_SET_PATH_HASH_MODE (0x3D) introduced. TRACE flags byte low 2 bits carry path hash size.
11 CMD_SEND_CHANNEL_DATA (0x3E), PACKET_CHANNEL_DATA_RECV (0x1B); CMD_SET_DEFAULT_FLOOD_SCOPE (0x3F) / CMD_GET_DEFAULT_FLOOD_SCOPE (0x40) + PACKET_DEFAULT_FLOOD_SCOPE (0x1C).

Compatibility obligations. A host implementation targeting version N MUST be prepared to operate against a node that reports any fw_ver ≤ N by falling back on the capabilities the lower version advertises. Conversely, a host MAY operate against a node reporting fw_ver > N: the node will interpret unknown host commands through ERR_UNSUPPORTED and will limit which trailing fields it emits based on min(app_target_ver, fw_ver). Hosts SHOULD NOT send a command whose opcode was introduced at a version higher than the node’s reported fw_ver.

A host SHOULD always send CMD_DEVICE_QUERY immediately after CMD_APP_START to establish capability.


2.4. Initialization Sequence

The following sequence is the recommended host-library convention for bringing up a companion session. The firmware does not enforce ordering beyond the natural preconditions — for example, the host cannot meaningfully interpret optional fields of PACKET_DEVICE_INFO before it knows the negotiated protocol version, which is only reported in response to CMD_DEVICE_QUERY. A host MAY deviate from this sequence when it has cached state from a prior session (e.g., skip CMD_GET_CONTACTS when contacts were enumerated recently and no push indicates a change).

On every new host connection, a conforming host SHOULD execute the following sequence:

  1. Connect (transport-specific).
  2. Enable notifications (BLE only).
  3. CMD_APP_START — identifies the host to the node, receives PACKET_SELF_INFO.
  4. CMD_DEVICE_QUERY — negotiates protocol version, receives PACKET_DEVICE_INFO.
  5. CMD_SET_DEVICE_TIME — sets the node’s real-time clock to the host’s current time.
  6. CMD_GET_CONTACTS — enumerates contacts.
  7. CMD_GET_CHANNEL — for each channel slot the node supports, enumerate channels.
  8. CMD_SYNC_NEXT_MESSAGE — drain the queue of messages the node buffered while disconnected.

Beyond step 8, the host operates asynchronously: it sends commands on user actions, and handles push notifications as they arrive.


2.5. Commands

Each command is identified by its command code (the first byte of the frame). Command codes are grouped logically below; the reference numeric assignments are in Appendix B.

For brevity, the response column shows the most common response; many commands can also return PACKET_ERROR with an error code (§2.8). Any command that produces no response is noted explicitly.

2.5.1. Session and Device

Code Name Response
0x01 CMD_APP_START PACKET_SELF_INFO
0x16 CMD_DEVICE_QUERY PACKET_DEVICE_INFO
0x13 CMD_REBOOT (none; connection drops)
0x33 CMD_FACTORY_RESET PACKET_OK
0x14 CMD_GET_BATT_AND_STORAGE PACKET_BATTERY
0x38 CMD_GET_STATS PACKET_STATS
0x05 CMD_GET_DEVICE_TIME PACKET_CURR_TIME
0x06 CMD_SET_DEVICE_TIME PACKET_OK
0x25 CMD_SET_DEVICE_PIN PACKET_OK
0x1C CMD_HAS_CONNECTION PACKET_OK / PACKET_ERROR

CMD_APP_START — initializes the session.

┌──────┬──────────┬──────────┐
│ 0x01 │ reserved │ app_name │
│  1 B │  7 bytes │ 0–N B    │
└──────┴──────────┴──────────┘

CMD_DEVICE_QUERY — fetches device info and negotiates protocol version.

┌──────┬────────────────┐
│ 0x16 │ app_target_ver │
│  1 B │     1 B        │
└──────┴────────────────┘

CMD_SET_DEVICE_TIME — sets the node’s clock.

┌──────┬──────────────┐
│ 0x06 │   timestamp  │
│  1 B │  4 B (LE)    │
└──────┴──────────────┘

CMD_GET_STATS — queries a statistics category.

┌──────┬────────────┐
│ 0x38 │ stats_type │
│  1 B │    1 B     │
└──────┴────────────┘

Where stats_type is:

See §2.6 for the response formats.

CMD_SET_DEVICE_PIN — sets the 6-digit BLE bonding PIN.

┌──────┬───────┐
│ 0x25 │  pin  │
│  1 B │  4 B  │
└──────┴───────┘

pin is an unsigned 32-bit integer (little-endian), value 0–999999. A value of 0 disables the PIN.

2.5.2. Identity

Code Name Response
0x17 CMD_EXPORT_PRIVATE_KEY PACKET_PRIVATE_KEY
0x18 CMD_IMPORT_PRIVATE_KEY PACKET_OK
0x21 CMD_SIGN_START PACKET_SIGN_START
0x22 CMD_SIGN_DATA PACKET_OK
0x23 CMD_SIGN_FINISH PACKET_SIGNATURE

Signing flow. To produce an Ed25519 signature of an arbitrarily-large buffer, the host streams the data to the node in chunks and then retrieves the signature:

  1. The host sends CMD_SIGN_START (no payload) to begin a new session. The node allocates a buffer of up to MAX_SIGN_DATA_LEN bytes (8192 in the reference firmware) and replies with PACKET_SIGN_START (§2.6.14) carrying that maximum.
  2. The host sends one or more CMD_SIGN_DATA frames, each carrying a chunk of bytes to append. The combined size of all chunks across a single session MUST NOT exceed the max_len reported by PACKET_SIGN_START; an over-sized chunk is rejected with PACKET_ERROR + ERR_TABLE_FULL. Chunks beyond the session’s buffer are rejected before any append; the session remains valid and further chunks MAY be sent, up to the limit.
  3. The host sends CMD_SIGN_FINISH (no payload). The node computes the Ed25519 signature over the concatenated chunks, deallocates the buffer, and replies with PACKET_SIGNATURE (§2.6.15).

Calling CMD_SIGN_DATA or CMD_SIGN_FINISH without a prior CMD_SIGN_START returns PACKET_ERROR + ERR_BAD_STATE. Only one signing session is active at a time; a new CMD_SIGN_START discards any in-progress buffer.

CMD_SIGN_START — begin a signing session.

┌──────┐
│ 0x21 │
│  1 B │
└──────┘

CMD_SIGN_DATA — append a chunk to the session buffer.

┌──────┬──────────────────┐
│ 0x22 │      chunk       │
│  1 B │    variable      │
└──────┴──────────────────┘

CMD_SIGN_FINISH — compute and return the signature.

┌──────┐
│ 0x23 │
│  1 B │
└──────┘

2.5.3. Radio and Network Configuration

Code Name Response
0x0B CMD_SET_RADIO_PARAMS PACKET_OK
0x0C CMD_SET_RADIO_TX_POWER PACKET_OK
0x15 CMD_SET_TUNING_PARAMS PACKET_OK
0x2B CMD_GET_TUNING_PARAMS PACKET_TUNING_PARAMS
0x26 CMD_SET_OTHER_PARAMS PACKET_OK
0x36 CMD_SET_FLOOD_SCOPE_KEY PACKET_OK
0x3C CMD_GET_ALLOWED_REPEAT_FREQ PACKET_ALLOWED_REPEAT_FREQ
0x3D CMD_SET_PATH_HASH_MODE PACKET_OK
0x3F CMD_SET_DEFAULT_FLOOD_SCOPE PACKET_OK
0x40 CMD_GET_DEFAULT_FLOOD_SCOPE PACKET_DEFAULT_FLOOD_SCOPE

CMD_SET_RADIO_PARAMS — sets the LoRa modem parameters.

┌──────┬──────────┬──────────┬─────┬─────┐
│ 0x0B │   freq   │    bw    │ sf  │ cr  │
│  1 B │   4 B    │   4 B    │ 1 B │ 1 B │
└──────┴──────────┴──────────┴─────┴─────┘

CMD_SET_RADIO_TX_POWER — sets the LoRa transmit power.

┌──────┬────────────┐
│ 0x0C │  tx_power  │
│  1 B │  1 B int8  │
└──────┴────────────┘

CMD_SET_TUNING_PARAMS — tunes internal timing parameters that affect retransmit scheduling.

┌──────┬──────────────────┬──────────────────┐
│ 0x15 │  rx_delay_base   │ airtime_factor   │
│  1 B │     4 B LE       │     4 B LE       │
└──────┴──────────────────┴──────────────────┘

CMD_GET_TUNING_PARAMS — retrieves the current tuning parameters. No payload beyond the command code. Response: PACKET_TUNING_PARAMS (§2.6.13).

CMD_SET_OTHER_PARAMS — configures miscellaneous node behaviour. All trailing bytes after the first are optional and introduce capability incrementally; a host SHOULD send only as many bytes as its target protocol version supports (see §2.3).

┌──────┬─────────────────────┬──────────────────┬──────────────────┬─────────────┐
│ 0x26 │ manual_add_contacts │ telemetry_modes  │ advert_loc_policy│ multi_acks  │
│  1 B │       1 B           │  1 B (v5+, opt)  │  1 B (v?+, opt)  │ 1 B (v7+, opt)│
└──────┴─────────────────────┴──────────────────┴──────────────────┴─────────────┘

CMD_SET_FLOOD_SCOPE_KEY — sets the transport key used to derive transport_code_1 for outgoing transport-scoped packets (the “current” scope, applied to the next sends until cleared or replaced). Previously known as CMD_SET_FLOOD_SCOPE in some earlier documentation; the opcode (0x36) is unchanged.

┌──────┬──────────┬──────────────────┐
│ 0x36 │ reserved │  transport_key   │
│  1 B │   1 B    │    16 bytes      │
└──────┴──────────┴──────────────────┘

CMD_SET_PATH_HASH_MODE — sets the per-hash size (see Part 1, §2.5) used for outgoing flood packets.

┌──────┬──────────┬──────┐
│ 0x3D │ reserved │ mode │
│  1 B │   1 B    │ 1 B  │
└──────┴──────────┴──────┘

CMD_SET_DEFAULT_FLOOD_SCOPE — sets the default transport key (and human-readable scope name) that the node will apply to transport-scoped sends in the absence of an explicitly-configured current scope (§CMD_SET_FLOOD_SCOPE_KEY). The default scope persists across reboots; the current scope does not.

┌──────┬──────────────────┬──────────────────┐
│ 0x3F │   scope_name     │   transport_key  │
│  1 B │   31 B (padded)  │     16 bytes     │
└──────┴──────────────────┴──────────────────┘

A frame consisting of just the 1-byte command code (total length 1) clears the stored default scope.

CMD_GET_DEFAULT_FLOOD_SCOPE — returns the currently-stored default scope.

┌──────┐
│ 0x40 │
│  1 B │
└──────┘

The response is PACKET_DEFAULT_FLOOD_SCOPE (§2.6.12).

2.5.4. Contacts

Code Name Response
0x04 CMD_GET_CONTACTS PACKET_CONTACT_STARTPACKET_CONTACT* → PACKET_CONTACT_END
0x1E CMD_GET_CONTACT_BY_KEY PACKET_CONTACT / PACKET_ERROR
0x09 CMD_ADD_UPDATE_CONTACT PACKET_OK
0x0F CMD_REMOVE_CONTACT PACKET_OK
0x0D CMD_RESET_PATH PACKET_OK
0x10 CMD_SHARE_CONTACT PACKET_OK
0x11 CMD_EXPORT_CONTACT PACKET_EXPORT_CONTACT
0x12 CMD_IMPORT_CONTACT PACKET_OK
0x3A CMD_SET_AUTOADD_CONFIG PACKET_OK
0x3B CMD_GET_AUTOADD_CONFIG PACKET_AUTOADD_CONFIG

CMD_GET_CONTACTS — enumerates contacts. Optionally specifies a “since” filter.

┌──────┬────────────────┐
│ 0x04 │  since (opt.)  │
│  1 B │  4 B LE        │
└──────┴────────────────┘

If since is present and non-zero, only contacts whose lastmod ≥ since are returned.

CMD_ADD_UPDATE_CONTACT frame layout. The frame is 136 bytes when no optional fields are present; 144 bytes when the gps_lat/gps_lon pair is appended; and 148 bytes when lastmod is additionally appended. Optional fields MUST be appended in the order shown (location before lastmod); lastmod MUST NOT appear without the location pair preceding it.

┌──────┬──────────┬──────┬───────┬──────────────┬──────────┬──────┬───────────────────────┐
│ 0x09 │ pub_key  │ type │ flags │ out_path_len │ out_path │ name │ last_advert_timestamp │
│  1 B │   32 B   │ 1 B  │  1 B  │     1 B      │   64 B   │ 32 B │        4 B            │
└──────┴──────────┴──────┴───────┴──────────────┴──────────┴──────┴───────────────────────┘
   + optional (all-or-none as a pair): gps_lat (4 B) + gps_lon (4 B)
   + optional (only valid with location present): lastmod (4 B)

CMD_SET_AUTOADD_CONFIG — sets auto-add behaviour when an unknown node’s advert arrives.

┌──────┬───────┬──────────┐
│ 0x3A │ flags │ max_hops │
│  1 B │  1 B  │   1 B    │
└──────┴───────┴──────────┘

flags is a bitmask:

Bit Mask Name
0 0x01 Overwrite oldest when full
1 0x02 Auto-add chat nodes
2 0x04 Auto-add repeaters
3 0x08 Auto-add room servers
4 0x10 Auto-add sensors

max_hops — maximum observed path hop count before a node is eligible for auto-add.

2.5.5. Channels

Code Name Response
0x1F CMD_GET_CHANNEL PACKET_CHANNEL_INFO
0x20 CMD_SET_CHANNEL PACKET_OK

CMD_SET_CHANNEL — creates or updates a channel slot.

┌──────┬─────────────┬───────┬──────────┐
│ 0x20 │ channel_idx │ name  │  secret  │
│  1 B │     1 B     │ 32 B  │   16 B   │
└──────┴─────────────┴───────┴──────────┘

2.5.6. Messaging

Code Name Response
0x02 CMD_SEND_TXT_MSG PACKET_SENT
0x03 CMD_SEND_CHANNEL_TXT_MSG PACKET_OK
0x3E CMD_SEND_CHANNEL_DATA PACKET_OK
0x19 CMD_SEND_RAW_DATA PACKET_OK
0x32 CMD_SEND_BINARY_REQ PACKET_SENT
0x39 CMD_SEND_ANON_REQ PACKET_SENT
0x37 CMD_SEND_CONTROL_DATA PACKET_OK
0x0A CMD_SYNC_NEXT_MESSAGE PACKET_CONTACT_MSG_*, PACKET_CHANNEL_MSG_*, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS

CMD_SEND_TXT_MSG — sends a direct text message to a known contact.

┌──────┬──────────┬──────────┬───────────┬─────────────────┬──────────────┐
│ 0x02 │ txt_type │ attempt  │ timestamp │ pub_key_prefix  │  message     │
│  1 B │   1 B    │   1 B    │   4 B     │      6 B        │  variable    │
└──────┴──────────┴──────────┴───────────┴─────────────────┴──────────────┘

The minimum valid frame length is 14 bytes (13-byte header + at least one byte of text).

Note: Some earlier drafts of this specification listed pub_key as 32 bytes at this offset. The reference firmware reads exactly 6 bytes. Hosts that send 32 bytes here will cause the node to interpret 26 bytes of pubkey tail as the beginning of the message text, leading to silent corruption.

CMD_SEND_CHANNEL_TXT_MSG — sends a group text message.

┌──────┬──────────┬─────────────┬───────────┬──────────────┐
│ 0x03 │ txt_type │ channel_idx │ timestamp │   message    │
│  1 B │   1 B    │     1 B     │   4 B     │  variable    │
└──────┴──────────┴─────────────┴───────────┴──────────────┘

CMD_SYNC_NEXT_MESSAGE — pulls the next queued received message (if any).

┌──────┐
│ 0x0A │
│  1 B │
└──────┘

Response is one of PACKET_CONTACT_MSG_RECV, PACKET_CONTACT_MSG_V3, PACKET_CHANNEL_MSG_RECV, PACKET_CHANNEL_MSG_V3, PACKET_CHANNEL_DATA_RECV, or PACKET_NO_MORE_MSGS (§2.6).

CMD_SEND_CHANNEL_DATA — sends a non-text datagram on a group channel.

┌──────┬─────────────┬──────────┬──────────────┬───────────┬──────────┐
│ 0x3E │ channel_idx │ path_len │    path      │ data_type │ payload  │
│  1 B │     1 B     │   1 B    │ variable (*) │  2 B LE   │ variable │
└──────┴─────────────┴──────────┴──────────────┴───────────┴──────────┘

The node responds with PACKET_OK on success, PACKET_ERROR + ERR_NOT_FOUND if channel_idx is invalid, PACKET_ERROR + ERR_ILLEGAL_ARG if data_type == 0x0000 or the payload exceeds the size limit, and PACKET_ERROR + ERR_TABLE_FULL if the outbound packet pool is exhausted.

2.5.7. Advertisements and Path Discovery

Code Name Response
0x07 CMD_SEND_SELF_ADVERT PACKET_OK
0x08 CMD_SET_ADVERT_NAME PACKET_OK
0x0E CMD_SET_ADVERT_LATLON PACKET_OK
0x2A CMD_GET_ADVERT_PATH PACKET_ADVERT_PATH
0x34 CMD_SEND_PATH_DISCOVERY_REQ PACKET_SENT
0x24 CMD_SEND_TRACE_PATH PACKET_SENT

CMD_SEND_SELF_ADVERT — broadcasts a self-advert packet.

┌──────┬───────┐
│ 0x07 │ type  │
│  1 B │  1 B  │
└──────┴───────┘

type = 0 (flood) or 1 (zero-hop).

CMD_SET_ADVERT_NAME — sets the node’s display name that appears in outgoing adverts.

┌──────┬──────────────────┐
│ 0x08 │      name        │
│  1 B │    variable      │
└──────┴──────────────────┘

CMD_SET_ADVERT_LATLON — sets the location embedded in outgoing adverts.

┌──────┬──────────┬──────────┐
│ 0x0E │ latitude │ longitude│
│  1 B │  4 B LE  │  4 B LE  │
└──────┴──────────┴──────────┘

Values are signed 32-bit integers representing degrees × 1,000,000. Out-of-range coordinates (outside ±90°/±180°) are rejected with PACKET_ERROR + ERR_ILLEGAL_ARG. A trailing 4-byte altitude field is reserved for future use and currently parsed-but-unused.

CMD_GET_ADVERT_PATH — retrieves the most recently observed path to a given peer from the node’s advert-path cache.

┌──────┬──────────┬─────────────┐
│ 0x2A │ reserved │   pub_key   │
│  1 B │   1 B    │    32 B     │
└──────┴──────────┴─────────────┘

Response: PACKET_ADVERT_PATH (§2.6.17), or PACKET_ERROR + ERR_NOT_FOUND if no path for that peer is cached.

CMD_SEND_PATH_DISCOVERY_REQ — originates a flood-routed telemetry request to a known contact, used by the host to prompt the peer to return its preferred path via the resulting PATH reply. Implementation-wise this is a CMD_SEND_TELEMETRY_REQ forced to flood-routing.

┌──────┬──────────┬─────────────┐
│ 0x34 │ reserved │   pub_key   │
│  1 B │   1 B    │    32 B     │
└──────┴──────────┴─────────────┘

The node responds with PACKET_SENT carrying a tag (§2.6.2) to be matched against the eventual PUSH_CODE_PATH_DISCOVERY_RESP (§2.7). If the contact does not exist in the node’s contact table, PACKET_ERROR + ERR_NOT_FOUND is returned.

CMD_SEND_TRACE_PATH — originates a TRACE packet (see Part 1, §2.13) along a host-specified path.

┌──────┬─────┬──────────┬───────┬──────────────┐
│ 0x24 │ tag │ auth_code│ flags │ path_hashes  │
│  1 B │ 4 B │   4 B    │  1 B  │   variable   │
└──────┴─────┴──────────┴───────┴──────────────┘

Response: PACKET_SENT carrying the same tag at offset 2 (§2.6.2).

2.5.8. Login / Status / Telemetry (server interactions)

Code Name Response
0x1A CMD_SEND_LOGIN PUSH_CODE_LOGIN_SUCCESS / PUSH_CODE_LOGIN_FAIL
0x1B CMD_SEND_STATUS_REQ PUSH_CODE_STATUS_RESPONSE
0x1D CMD_LOGOUT PACKET_OK
0x27 CMD_SEND_TELEMETRY_REQ PUSH_CODE_TELEMETRY_RESPONSE

These commands initiate an over-the-air exchange with a remote node (repeater, room server, sensor). Responses arrive asynchronously via push notifications (§2.7).

2.5.9. Custom Variables

Code Name Response
0x28 CMD_GET_CUSTOM_VARS PACKET_CUSTOM_VARS
0x29 CMD_SET_CUSTOM_VAR PACKET_OK

Custom variables are ASCII name/value pairs stored on the node and used for vendor-specific extensions (GPS tuning parameters, sensor calibration, experimental features). The namespace and supported names are implementation-specific; the Companion Protocol only defines the transport.

CMD_GET_CUSTOM_VARS — enumerate all current custom variables. No payload.

┌──────┐
│ 0x28 │
│  1 B │
└──────┘

Response: PACKET_CUSTOM_VARS (§2.6.16) carrying a comma-separated list of name:value pairs.

CMD_SET_CUSTOM_VAR — set a single custom variable.

┌──────┬────────────────────────────────┐
│ 0x29 │    "name:value"  (ASCII)       │
│  1 B │          variable              │
└──────┴────────────────────────────────┘

Because name and value are both variable-length and share a single ASCII payload, neither may contain the : byte. Hosts that need to carry binary data or reserved characters MUST encode them (e.g., hex or base64) at the application layer.


2.6. Responses

Response frames begin with a packet code byte.

Code Name Description
0x00 PACKET_OK Command succeeded. Optional 4-byte value follows.
0x01 PACKET_ERROR Command failed. Optional error code follows.
0x02 PACKET_CONTACT_START Start of contact enumeration; 4-byte total count.
0x03 PACKET_CONTACT One contact record. See §2.6.18.
0x04 PACKET_CONTACT_END End of contact enumeration.
0x05 PACKET_SELF_INFO Node’s self information. See §2.6.3.
0x06 PACKET_SENT Message transmission scheduled. See §2.6.2.
0x07 PACKET_CONTACT_MSG_RECV Received contact message (legacy, pre-v3). See §2.6.6.
0x08 PACKET_CHANNEL_MSG_RECV Received channel message (legacy, pre-v3). See §2.6.7.
0x09 PACKET_CURR_TIME Current device time. See §2.6.10.
0x0A PACKET_NO_MORE_MSGS No queued messages.
0x0B PACKET_EXPORT_CONTACT Exported contact blob.
0x0C PACKET_BATTERY Battery and storage. See §2.6.5.
0x0D PACKET_DEVICE_INFO Device information. See §2.6.4.
0x0E PACKET_PRIVATE_KEY Exported private key.
0x0F PACKET_DISABLED Feature not enabled.
0x10 PACKET_CONTACT_MSG_V3 Received contact message (v3, with SNR). See §2.6.6.
0x11 PACKET_CHANNEL_MSG_V3 Received channel message (v3, with SNR). See §2.6.7.
0x12 PACKET_CHANNEL_INFO Channel slot info. See §2.6.9.
0x13 PACKET_SIGN_START Signing session established. See §2.6.14.
0x14 PACKET_SIGNATURE Signing session signature result. See §2.6.15.
0x15 PACKET_CUSTOM_VARS Custom variable listing. See §2.6.16.
0x16 PACKET_ADVERT_PATH Cached advert path. See §2.6.17.
0x17 PACKET_TUNING_PARAMS Radio tuning parameters. See §2.6.13.
0x18 PACKET_STATS Statistics response. See §2.6.8.
0x19 PACKET_AUTOADD_CONFIG Auto-add configuration.
0x1A PACKET_ALLOWED_REPEAT_FREQ Allowed client-repeat frequency ranges. See §2.6.19.
0x1B PACKET_CHANNEL_DATA_RECV Received channel datagram (non-text). See §2.6.11.
0x1C PACKET_DEFAULT_FLOOD_SCOPE Stored default flood scope. See §2.6.12.

Code values 0x80 and above are push notifications (§2.7), not responses.

2.6.1. PACKET_OK and PACKET_ERROR

PACKET_OK:
┌──────┬────────────────┐
│ 0x00 │  value (opt.)  │
│  1 B │   4 B LE       │
└──────┴────────────────┘

PACKET_ERROR:
┌──────┬────────────────┐
│ 0x01 │ err_code (opt) │
│  1 B │   1 B          │
└──────┴────────────────┘

When PACKET_OK carries a value, it is command-dependent (e.g., a pending ACK’s expected checksum for CMD_SEND_TXT_MSG). When PACKET_ERROR carries an error code, see §2.8.

2.6.2. PACKET_SENT

Returned in response to commands that launch an over-the-air exchange the host will later correlate with an asynchronous response or acknowledgement: CMD_SEND_TXT_MSG, CMD_SEND_LOGIN, CMD_SEND_STATUS_REQ, CMD_SEND_ANON_REQ, CMD_SEND_TELEMETRY_REQ, CMD_SEND_BINARY_REQ, CMD_SEND_PATH_DISCOVERY_REQ, CMD_SEND_TRACE_PATH. Total frame length is 10 bytes.

Commands that send but expect no per-send correlation — CMD_SEND_CHANNEL_TXT_MSG, CMD_SEND_CHANNEL_DATA, CMD_SEND_RAW_DATA, CMD_SEND_CONTROL_DATA — return PACKET_OK instead.

┌──────┬─────────────┬───────────────────────────┬───────────────────┐
│ 0x06 │ send_method │  expected_ack │ tag (alt) │   est_timeout_ms  │
│  1 B │     1 B     │          4 B LE           │      4 B LE       │
└──────┴─────────────┴───────────────────────────┴───────────────────┘
Offset Size Field Description
0 1 code 0x06
1 1 send_method 0x00 = sent via direct (known path); 0x01 = sent via flood. Always 0x00 for CMD_SEND_TRACE_PATH.
2 4 expected_ack or tag Per-command correlation handle (see table below), 4-byte little-endian.
6 4 est_timeout_ms Node’s estimate, in milliseconds, of how long the host should wait before considering the send lost. Derived from the modem’s airtime estimate and the hop count.

The 4-byte correlation field at offset 2 is interpreted differently depending on the originating command:

Originating command Field semantics
CMD_SEND_TXT_MSG expected_ack — first 4 bytes of the SHA-256-derived ACK hash (see Part 1, §2.12). 0x00000000 when no ACK is expected (e.g. TXT_TYPE_CLI_DATA). Match against PUSH_CODE_SEND_CONFIRMED.
CMD_SEND_LOGIN expected_ack — first 4 bytes of the recipient’s public key (used by the node to route the eventual PUSH_CODE_LOGIN_SUCCESS / PUSH_CODE_LOGIN_FAIL).
CMD_SEND_STATUS_REQ expected_ack — first 4 bytes of the recipient’s public key (legacy scheme; matches the eventual PUSH_CODE_STATUS_RESPONSE).
CMD_SEND_ANON_REQ tag — caller-chosen request identifier, echoed in the eventual response push.
CMD_SEND_TELEMETRY_REQ tag — caller-chosen request identifier, echoed in PUSH_CODE_TELEMETRY_RESPONSE.
CMD_SEND_BINARY_REQ tag — caller-chosen request identifier, echoed in PUSH_CODE_BINARY_RESPONSE.
CMD_SEND_PATH_DISCOVERY_REQ tag — caller-chosen request identifier, echoed in PUSH_CODE_PATH_DISCOVERY_RESP.
CMD_SEND_TRACE_PATH tag — same 4-byte tag the host passed in the originating command, echoed in PUSH_CODE_TRACE_DATA.

A host that receives a PACKET_SENT with expected_ack == 0 for CMD_SEND_TXT_MSG SHOULD NOT wait for a PUSH_CODE_SEND_CONFIRMED frame: none will be emitted for that send. For tag-valued sends, the host SHOULD maintain a table keyed by tag and wait for the matching push up to est_timeout_ms.

2.6.3. PACKET_SELF_INFO

Returned in response to CMD_APP_START.

┌──────┬──────────┬──────────┬──────────────┬───────────┬────────────┬────────────┬─────────────┬─────────────────┬───────────────────┬───────┬────────┬──────────────┬──────────────┬─────┬─────┬──────┐
│ 0x05 │ adv_type │ tx_power │ max_tx_power │ pub_key   │   adv_lat  │  adv_lon   │ multi_acks  │ adv_loc_policy  │ telemetry_mode    │ manual│ freq   │   bw         │      sf      │ cr  │ name │      │
│  1 B │   1 B    │   1 B    │     1 B      │  32 B     │   4 B LE   │  4 B LE    │    1 B      │      1 B        │       1 B         │ add   │ 4 B LE │  4 B LE      │     1 B      │ 1 B │ var. │      │
│      │          │          │              │           │            │            │             │                 │                   │ 1 B   │        │              │              │     │      │      │
└──────┴──────────┴──────────┴──────────────┴───────────┴────────────┴────────────┴─────────────┴─────────────────┴───────────────────┴───────┴────────┴──────────────┴──────────────┴─────┴─────┴──────┘
Offset Size Field Description
0 1 code 0x05
1 1 adv_type Node’s ADV_TYPE_* value.
2 1 tx_power Current TX power (dBm).
3 1 max_tx_power Maximum allowed TX power (dBm).
4 32 pub_key Node’s Ed25519 public key.
36 4 adv_lat Advertised latitude. degrees × 1,000,000, signed LE.
40 4 adv_lon Advertised longitude. degrees × 1,000,000, signed LE.
44 1 multi_acks Multi-ACK count.
45 1 adv_loc_policy Advert location policy (implementation-defined).
46 1 telemetry_mode Packed: bits 4–5 = env, 2–3 = loc, 0–1 = base.
47 1 manual_add_contacts Boolean (0 or non-zero).
48 4 radio_freq Frequency in kHz × 1000 (i.e., Hz). Divide by 1000 for kHz.
52 4 radio_bw Bandwidth in kHz × 1000 (i.e., Hz).
56 1 radio_sf Spreading factor (5–12).
57 1 radio_cr Coding rate (5–8).
58+ var name UTF-8 node name, no null terminator.

2.6.4. PACKET_DEVICE_INFO

Returned in response to CMD_DEVICE_QUERY.

┌──────┬─────────┬──────────────┬──────────────┬─────────┬──────────────┬─────────┬──────────┬────────────┬──────────────────┐
│ 0x0D │ fw_ver  │ max_contacts │ max_channels │ ble_pin │   fw_build   │  model  │ version  │ repeat_ena │  path_hash_mode  │
│  1 B │   1 B   │     1 B      │     1 B      │  4 B LE │    12 B      │  40 B   │   20 B   │   1 B      │       1 B        │
└──────┴─────────┴──────────────┴──────────────┴─────────┴──────────────┴─────────┴──────────┴────────────┴──────────────────┘
Offset Size Field Description
0 1 code 0x0D
1 1 fw_ver Companion Protocol Version supported by the node (see §1.4). Confusingly named — this is a companion-protocol capability level, not a firmware release identifier. The human-readable firmware identifier is carried separately in the version field at offset 60. Hosts SHOULD use fw_ver for capability negotiation and the version / fw_build strings for display or logging only.
2 1 max_contacts Max contacts × 2 (multiply by 2 to get real value).
3 1 max_channels Max channel slots.
4 4 ble_pin BLE bonding PIN (0 = disabled). LE uint32.
8 12 fw_build Firmware build string, null-padded UTF-8.
20 40 model Hardware model string, null-padded UTF-8.
60 20 version Firmware version string, null-padded UTF-8.
80 1 repeat_enabled (present if fw_ver ≥ 9) Client-repeat mode.
81 1 path_hash_mode (present if fw_ver ≥ 10) 1, 2, or 3.

Trailing fields are version-gated: repeat_enabled is present only when the node’s fw_ver is 9 or greater, and path_hash_mode is present only at fw_ver ≥ 10. Each of these fields extends the frame by one byte beyond the previous version’s length — 80 bytes at fw_ver ≤ 8, 81 bytes at fw_ver = 9, 82 bytes at fw_ver ≥ 10. Hosts MUST use the frame length actually received — not the advertised fw_ver alone — as the authoritative signal of which optional fields are present, because a malformed or truncated frame can advertise one version and carry bytes for another. A host that trusts fw_ver without verifying length risks reading past the end of the buffer.

2.6.5. PACKET_BATTERY

┌──────┬──────────────┬─────────────┬──────────────┐
│ 0x0C │  battery_mv  │  used_kb    │   total_kb   │
│  1 B │    2 B LE    │   4 B LE    │    4 B LE    │
└──────┴──────────────┴─────────────┴──────────────┘
Offset Size Field Description
0 1 code 0x0C
1 2 battery_mv Battery voltage in millivolts, uint16 LE.
3 4 used_kb Used storage in kilobytes, uint32 LE.
7 4 total_kb Total storage in kilobytes, uint32 LE.

Total frame: 11 bytes. If only the 3-byte prefix is present, storage fields are unavailable.

2.6.6. PACKET_CONTACT_MSG_RECV / PACKET_CONTACT_MSG_V3

Legacy form (protocol version < 3):

┌──────┬──────────────┬──────────┬──────────┬───────────┬──────────────────┬───────────┐
│ 0x07 │ pubkey_prefix│ path_len │ txt_type │ timestamp │ signature (opt)  │  message  │
│  1 B │    6 B       │   1 B    │   1 B    │   4 B LE  │   4 B if txt=2   │ variable  │
└──────┴──────────────┴──────────┴──────────┴───────────┴──────────────────┴───────────┘

V3 form (protocol version ≥ 3) — adds SNR metadata and 2 reserved bytes:

┌──────┬──────┬──────────┬──────────────┬──────────┬──────────┬───────────┬──────────────────┬───────────┐
│ 0x10 │ snr  │ reserved │ pubkey_prefix│ path_len │ txt_type │ timestamp │ signature (opt)  │  message  │
│  1 B │ 1 B  │   2 B    │    6 B       │   1 B    │   1 B    │   4 B LE  │   4 B if txt=2   │ variable  │
└──────┴──────┴──────────┴──────────────┴──────────┴──────────┴───────────┴──────────────────┴───────────┘

2.6.7. PACKET_CHANNEL_MSG_RECV / PACKET_CHANNEL_MSG_V3

Legacy:

┌──────┬─────────────┬──────────┬──────────┬───────────┬───────────┐
│ 0x08 │ channel_idx │ path_len │ txt_type │ timestamp │  message  │
│  1 B │     1 B     │   1 B    │   1 B    │   4 B LE  │ variable  │
└──────┴─────────────┴──────────┴──────────┴───────────┴───────────┘

V3:

┌──────┬──────┬──────────┬─────────────┬──────────┬──────────┬───────────┬───────────┐
│ 0x11 │ snr  │ reserved │ channel_idx │ path_len │ txt_type │ timestamp │  message  │
│  1 B │ 1 B  │   2 B    │     1 B     │   1 B    │   1 B    │   4 B LE  │ variable  │
└──────┴──────┴──────────┴─────────────┴──────────┴──────────┴───────────┴───────────┘

2.6.8. PACKET_STATS

Response to CMD_GET_STATS. The second byte echoes the requested stats_type.

Core stats (stats_type = 0): 11 bytes total.

┌──────┬────────────┬──────────────┬──────────────┬──────────┬───────────┐
│ 0x18 │ 0x00 (core)│  battery_mv  │ uptime_secs  │  errors  │ queue_len │
│  1 B │    1 B     │    2 B LE    │   4 B LE     │  2 B LE  │    1 B    │
└──────┴────────────┴──────────────┴──────────────┴──────────┴───────────┘

Radio stats (stats_type = 1): 14 bytes total.

┌──────┬───────────┬─────────────┬──────────┬──────────┬─────────────┬─────────────┐
│ 0x18 │ 0x01 (rad)│ noise_floor │  rssi    │  snr     │ tx_air_secs │ rx_air_secs │
│  1 B │    1 B    │   2 B LE    │  1 B     │  1 B     │   4 B LE    │   4 B LE    │
│      │           │  (int16)    │ (int8)   │ (int8)   │  (uint32)   │  (uint32)   │
└──────┴───────────┴─────────────┴──────────┴──────────┴─────────────┴─────────────┘

Packet stats (stats_type = 2): 26 bytes (legacy) or 30 bytes (with recv_errors).

┌──────┬───────────┬──────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────────┐
│ 0x18 │ 0x02 (pkt)│ recv │ sent │ flood_tx │ direct_tx│ flood_rx │ direct_rx│ recv_errors  │
│  1 B │    1 B    │ 4 LE │ 4 LE │   4 LE   │   4 LE   │   4 LE   │   4 LE   │  4 LE (opt)  │
└──────┴───────────┴──────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────────┘

Hosts MUST accept frames of length ≥ 26 and SHOULD parse recv_errors only if the length is ≥ 30.

2.6.9. PACKET_CHANNEL_INFO

┌──────┬─────────────┬───────┬──────────┐
│ 0x12 │ channel_idx │ name  │  secret  │
│  1 B │     1 B     │ 32 B  │   16 B   │
└──────┴─────────────┴───────┴──────────┘

An all-zero secret indicates an empty slot.

2.6.10. PACKET_CURR_TIME

┌──────┬──────────────┐
│ 0x09 │  timestamp   │
│  1 B │   4 B LE     │
└──────┴──────────────┘

2.6.11. PACKET_CHANNEL_DATA_RECV

Delivered in response to CMD_SYNC_NEXT_MESSAGE when the node has a received channel datagram to hand to the host. Channel datagrams are distinct from channel text messages (§2.6.7): they carry a 16-bit data_type tag and an opaque binary payload rather than UTF-8 text.

┌──────┬──────┬──────────┬─────────────┬──────────┬───────────┬──────────┬──────────┐
│ 0x1B │ snr  │ reserved │ channel_idx │ path_len │ data_type │ data_len │ payload  │
│  1 B │ 1 B  │   2 B    │     1 B     │   1 B    │   2 B LE  │   1 B    │ variable │
└──────┴──────┴──────────┴─────────────┴──────────┴───────────┴──────────┴──────────┘
Offset Size Field Description
0 1 code 0x1B
1 1 snr Signed 8-bit, SNR_dB × 4 (0.25 dB resolution).
2 2 reserved Emitted as zero; hosts MUST ignore.
4 1 channel_idx Slot index of the channel the datagram arrived on.
5 1 path_len Encoded path length (see Part 1, §2.5) of the path the packet traversed when the datagram was flood-routed. 0xFF indicates the packet arrived via direct routing (no recorded path).
6 2 data_type Application-defined 16-bit datagram sub-type, little-endian. Matches the data_type the sender passed to CMD_SEND_CHANNEL_DATA.
8 1 data_len Length of payload in bytes.
9 var payload Up to MAX_CHANNEL_DATA_LENGTH (163) bytes of application-defined content.

A host that receives a PACKET_CHANNEL_DATA_RECV for a data_type it does not recognise SHOULD discard it silently rather than treating it as an error; the developer-namespace value 0xFFFF is explicitly reserved for experimentation and may appear from any peer on a shared channel.

2.6.12. PACKET_DEFAULT_FLOOD_SCOPE

Returned in response to CMD_GET_DEFAULT_FLOOD_SCOPE. Two forms exist:

No default scope configured (1 byte total):

┌──────┐
│ 0x1C │
│  1 B │
└──────┘

Default scope present (48 bytes total):

┌──────┬──────────────────┬──────────────────┐
│ 0x1C │   scope_name     │   transport_key  │
│  1 B │   31 B (padded)  │     16 bytes     │
└──────┴──────────────────┴──────────────────┘

Hosts MUST discriminate the two forms by the received frame length.

2.6.13. PACKET_TUNING_PARAMS

Returned in response to CMD_GET_TUNING_PARAMS. Total frame length is 9 bytes.

┌──────┬──────────────────┬──────────────────┐
│ 0x17 │  rx_delay_base   │ airtime_factor   │
│  1 B │     4 B LE       │     4 B LE       │
└──────┴──────────────────┴──────────────────┘

2.6.14. PACKET_SIGN_START

Returned in response to CMD_SIGN_START. Total frame length is 6 bytes.

┌──────┬──────────┬──────────────────┐
│ 0x13 │ reserved │     max_len      │
│  1 B │   1 B    │     4 B LE       │
└──────┴──────────┴──────────────────┘

2.6.15. PACKET_SIGNATURE

Returned in response to CMD_SIGN_FINISH. Total frame length is 65 bytes.

┌──────┬─────────────────┐
│ 0x14 │    signature    │
│  1 B │     64 bytes    │
└──────┴─────────────────┘

2.6.16. PACKET_CUSTOM_VARS

Returned in response to CMD_GET_CUSTOM_VARS.

┌──────┬──────────────────────────────────────────────────────────┐
│ 0x15 │    "name1:value1,name2:value2,..."  (ASCII)              │
│  1 B │                    variable                              │
└──────┴──────────────────────────────────────────────────────────┘

2.6.17. PACKET_ADVERT_PATH

Returned in response to CMD_GET_ADVERT_PATH when a cached path exists.

┌──────┬─────────────────┬──────────┬──────────┐
│ 0x16 │ recv_timestamp  │ path_len │   path   │
│  1 B │    4 B LE       │   1 B    │ variable │
└──────┴─────────────────┴──────────┴──────────┘

If the queried peer has no entry in the advert-path cache, the node returns PACKET_ERROR + ERR_NOT_FOUND instead.

2.6.18. PACKET_CONTACT and other ContactInfo frames

The following frames all carry a 148-byte ContactInfo body after their 1-byte code:

The ContactInfo layout is:

┌──────────┬──────┬───────┬──────────────┬──────────┬──────┬───────────────────────┬──────────┬──────────┬──────────┐
│ pub_key  │ type │ flags │ out_path_len │ out_path │ name │ last_advert_timestamp │ gps_lat  │ gps_lon  │ lastmod  │
│   32 B   │ 1 B  │  1 B  │     1 B      │   64 B   │ 32 B │         4 B           │   4 B    │   4 B    │   4 B    │
└──────────┴──────┴───────┴──────────────┴──────────┴──────┴───────────────────────┴──────────┴──────────┴──────────┘

Field semantics are as defined for CMD_ADD_UPDATE_CONTACT in §2.5.4. Outbound frames from the node ALWAYS include the full 147-byte ContactInfo body (code byte + 147 = 148 total). This is asymmetric with the inbound CMD_ADD_UPDATE_CONTACT command, for which the trailing gps_lat/gps_lon/lastmod fields are optional: inbound, hosts MAY omit them and the node will fall back to its current RTC time; outbound, the node always emits them, using the contact’s stored values.

2.6.19. PACKET_ALLOWED_REPEAT_FREQ

Returned in response to CMD_GET_ALLOWED_REPEAT_FREQ. Communicates the set of frequency ranges in which this node is permitted to act as a client-repeater.

┌──────┬──────────────────────────────────────────────────────┐
│ 0x1A │   [ lower_freq (4 LE) | upper_freq (4 LE) ] × N      │
│  1 B │               8 × N bytes                            │
└──────┴──────────────────────────────────────────────────────┘

2.7. Push Notifications

Push notifications are unsolicited frames sent from the node to the host. Their code byte is always ≥ 0x80.

Code Name Data
0x80 PUSH_CODE_ADVERT Advertisement received (pubkey prefix).
0x81 PUSH_CODE_PATH_UPDATED A contact’s path was updated (pubkey prefix).
0x82 PUSH_CODE_SEND_CONFIRMED An outgoing message was ACKed (ACK checksum).
0x83 PUSH_CODE_MSG_WAITING One or more messages are queued; host should call CMD_SYNC_NEXT_MESSAGE.
0x84 PUSH_CODE_RAW_DATA Raw custom packet received.
0x85 PUSH_CODE_LOGIN_SUCCESS Login attempt succeeded.
0x86 PUSH_CODE_LOGIN_FAIL Login attempt failed.
0x87 PUSH_CODE_STATUS_RESPONSE Status request response.
0x88 PUSH_CODE_LOG_RX_DATA Raw RF log data (SNR, RSSI, bytes).
0x89 PUSH_CODE_TRACE_DATA Trace route response.
0x8A PUSH_CODE_NEW_ADVERT Advert from an unknown (new) node.
0x8B PUSH_CODE_TELEMETRY_RESPONSE Telemetry response (CayenneLPP).
0x8C PUSH_CODE_BINARY_RESPONSE Binary request response.
0x8D PUSH_CODE_PATH_DISCOVERY_RESP Path discovery response.
0x8E PUSH_CODE_CONTROL_DATA Control data packet received (e.g., DISCOVER_RESP).
0x8F PUSH_CODE_CONTACT_DELETED A contact was deleted (pubkey prefix).
0x90 PUSH_CODE_CONTACTS_FULL Contact table is full; auto-add failed.

2.7.1. PUSH_CODE_ADVERT

Emitted when an advert is received from a peer already known as a contact. 33 bytes total.

┌──────┬─────────────┐
│ 0x80 │   pub_key   │
│  1 B │    32 B     │
└──────┴─────────────┘

2.7.2. PUSH_CODE_PATH_UPDATED

Emitted when the node has learned a new path to a known contact (e.g., in response to a returned PATH reply). 33 bytes total. Same layout as PUSH_CODE_ADVERT.

┌──────┬─────────────┐
│ 0x81 │   pub_key   │
│  1 B │    32 B     │
└──────┴─────────────┘

2.7.3. PUSH_CODE_SEND_CONFIRMED

Emitted when the node receives an ACK for a previously-sent message, confirming delivery. 9 bytes total.

┌──────┬───────────┬─────────────────┐
│ 0x82 │ ack_hash  │  trip_time_ms   │
│  1 B │    4 B    │     4 B LE      │
└──────┴───────────┴─────────────────┘

The same ACK may be received and delivered to the host multiple times if the receiving peer retransmits; hosts SHOULD deduplicate by ack_hash.

2.7.4. PUSH_CODE_MSG_WAITING

Emitted as a 1-byte tickle to inform the host that one or more messages are queued and ready to be drained with CMD_SYNC_NEXT_MESSAGE.

┌──────┐
│ 0x83 │
│  1 B │
└──────┘

Carries no payload. Hosts SHOULD respond by issuing CMD_SYNC_NEXT_MESSAGE repeatedly until the node returns PACKET_NO_MORE_MSGS.

2.7.5. PUSH_CODE_RAW_DATA

Emitted when a PAYLOAD_TYPE_RAW_CUSTOM packet is received.

┌──────┬──────┬──────┬──────────┬──────────┐
│ 0x84 │ snr  │ rssi │ reserved │ payload  │
│  1 B │ 1 B  │ 1 B  │   1 B    │ variable │
└──────┴──────┴──────┴──────────┴──────────┘

2.7.6. PUSH_CODE_LOGIN_SUCCESS

Emitted when a pending CMD_SEND_LOGIN receives a successful login reply from the remote server. Two frame lengths exist depending on server protocol:

Legacy “OK” reply (8 bytes):

┌──────┬─────────────┬─────────────────┐
│ 0x85 │ is_admin    │ pubkey_prefix   │
│  1 B │ 1 B (= 0)   │     6 B         │
└──────┴─────────────┴─────────────────┘

Modern reply (15 bytes):

┌──────┬─────────────┬─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ 0x85 │ permissions │ pubkey_prefix   │ server_timestamp │ acl_permissions  │ fw_ver_level     │
│  1 B │    1 B      │     6 B         │     4 B LE       │       1 B        │       1 B        │
└──────┴─────────────┴─────────────────┴──────────────────┴──────────────────┴──────────────────┘

Hosts MUST discriminate between the two forms by the received frame length.

2.7.7. PUSH_CODE_LOGIN_FAIL

Emitted when a pending CMD_SEND_LOGIN receives a non-OK reply, or times out at the protocol layer. 8 bytes total.

┌──────┬──────────┬─────────────────┐
│ 0x86 │ reserved │ pubkey_prefix   │
│  1 B │   1 B    │     6 B         │
└──────┴──────────┴─────────────────┘

2.7.8. PUSH_CODE_STATUS_RESPONSE

Emitted when a pending CMD_SEND_STATUS_REQ receives a reply.

┌──────┬──────────┬─────────────────┬──────────────────┐
│ 0x87 │ reserved │ pubkey_prefix   │   status_data    │
│  1 B │   1 B    │     6 B         │     variable     │
└──────┴──────────┴─────────────────┴──────────────────┘

2.7.9. PUSH_CODE_LOG_RX_DATA

Emitted for every received RF packet when RF logging is enabled on the node. Useful for debugging and protocol analysis.

┌──────┬──────┬──────┬──────────────┐
│ 0x88 │ snr  │ rssi │  raw_packet  │
│  1 B │ 1 B  │ 1 B  │  0–255 B     │
└──────┴──────┴──────┴──────────────┘

2.7.10. PUSH_CODE_TRACE_DATA

Emitted when a TRACE packet (see Part 1, §2.13) reaches the node as its final destination. Delivers the full reconstructed trace to the host.

┌──────┬──────────┬──────────┬───────┬─────┬──────────┬─────────────┬──────────┬────────────┐
│ 0x89 │ reserved │ path_len │ flags │ tag │ auth_code│ path_hashes │ path_snrs│ final_snr  │
│  1 B │   1 B    │   1 B    │  1 B  │ 4 B │   4 B    │  variable   │ variable │  1 B int8  │
└──────┴──────────┴──────────┴───────┴─────┴──────────┴─────────────┴──────────┴────────────┘

2.7.11. PUSH_CODE_NEW_ADVERT

Emitted when an advert is received from an unknown peer that has been auto-added (or could be auto-added) to the contact table. 148 bytes total — identical layout to PACKET_CONTACT (§2.6.18).

┌──────┬──────────────────────────────────────────────────┐
│ 0x8A │              ContactInfo (147 bytes)             │
│  1 B │          pub_key + type + flags + ...            │
└──────┴──────────────────────────────────────────────────┘

See §2.6.18 for the full ContactInfo layout.

2.7.12. PUSH_CODE_TELEMETRY_RESPONSE

Emitted when a pending CMD_SEND_TELEMETRY_REQ receives a reply.

┌──────┬──────────┬─────────────────┬──────────────────────┐
│ 0x8B │ reserved │ pubkey_prefix   │   cayenne_lpp_data   │
│  1 B │   1 B    │     6 B         │      variable        │
└──────┴──────────┴─────────────────┴──────────────────────┘

2.7.13. PUSH_CODE_BINARY_RESPONSE

Emitted when a pending CMD_SEND_BINARY_REQ receives a reply.

┌──────┬──────────┬──────────┬──────────────────────┐
│ 0x8C │ reserved │   tag    │    response_data     │
│  1 B │   1 B    │   4 B    │       variable       │
└──────┴──────────┴──────────┴──────────────────────┘

Unlike status/telemetry/login pushes, PUSH_CODE_BINARY_RESPONSE carries a tag rather than a pubkey_prefix, because the host correlates binary requests and their responses by caller-chosen tag.

2.7.14. PUSH_CODE_PATH_DISCOVERY_RESP

Emitted when a pending CMD_SEND_PATH_DISCOVERY_REQ receives a path reply.

┌──────┬──────────┬─────────────────┬───────────────┬──────────┬──────────────┬──────────┐
│ 0x8D │ reserved │ pubkey_prefix   │ out_path_len  │ out_path │ in_path_len  │ in_path  │
│  1 B │   1 B    │     6 B         │     1 B       │ variable │     1 B      │ variable │
└──────┴──────────┴─────────────────┴───────────────┴──────────┴──────────────┴──────────┘

2.7.15. PUSH_CODE_CONTROL_DATA

Emitted when a zero-hop PAYLOAD_TYPE_CONTROL packet is received (see Part 1, §2.15). Used for DISCOVER_* discovery exchanges.

┌──────┬──────┬──────┬──────────┬──────────┐
│ 0x8E │ snr  │ rssi │ path_len │ payload  │
│  1 B │ 1 B  │ 1 B  │   1 B    │ variable │
└──────┴──────┴──────┴──────────┴──────────┘

2.7.16. PUSH_CODE_CONTACT_DELETED

Emitted when the node has deleted a contact — typically due to the auto-add “overwrite oldest when full” policy evicting an existing contact to make room for a new one. 33 bytes total.

┌──────┬─────────────┐
│ 0x8F │   pub_key   │
│  1 B │    32 B     │
└──────┴─────────────┘

2.7.17. PUSH_CODE_CONTACTS_FULL

Emitted when an inbound advert would have been auto-added but the contact table is full and no eviction is permitted (the AUTO_ADD_OVERWRITE_OLDEST flag is not set). 1 byte total.

┌──────┐
│ 0x90 │
│  1 B │
└──────┘

Carries no payload. Hosts SHOULD surface this to the user as an indication that manual intervention may be required to accept the new peer.


2.8. Error Codes

When a command fails, the node responds with PACKET_ERROR (0x01) followed by a single error-code byte.

Code Name Meaning
0x01 ERR_UNSUPPORTED The command or a requested feature is not supported.
0x02 ERR_NOT_FOUND The referenced item (contact, channel slot) does not exist.
0x03 ERR_TABLE_FULL The target table (contacts, channels) is full.
0x04 ERR_BAD_STATE The node is in a state that forbids this command (e.g., contact iteration in progress).
0x05 ERR_FILE_IO A persistent-storage operation failed (e.g., filesystem format during CMD_FACTORY_RESET).
0x06 ERR_ILLEGAL_ARG A command argument was malformed or out of range.

Hosts MUST tolerate additional, unknown error codes and SHOULD treat them as “generic error”.


Appendix A: Compliance Checklist

A minimally compliant implementation of the Companion Protocol MUST:

  1. Deliver exactly one frame per transport-level write or notification (§2.1.4).
  2. Implement the initialization sequence in §2.4.
  3. Implement at least: CMD_APP_START, CMD_DEVICE_QUERY, CMD_GET_CONTACTS, CMD_ADD_UPDATE_CONTACT, CMD_SEND_TXT_MSG, CMD_SYNC_NEXT_MESSAGE, CMD_SET_DEVICE_TIME.
  4. Respond with PACKET_ERROR + ERR_UNSUPPORTED to unknown command codes.
  5. Emit PUSH_CODE_MSG_WAITING when queued messages are available.
  6. If the implementation supports BLE, enforce one-frame-per-GATT-operation (§2.1.4) by refusing to send frames that do not fit in the negotiated MTU.
  7. If the implementation supports a stream transport, emit the directional marker matching its role (0x3E from a node, 0x3C from a host, §2.1.2) on all outbound frames.

A fully compliant implementation should additionally support channel messaging (CMD_GET_CHANNEL, CMD_SET_CHANNEL, CMD_SEND_CHANNEL_TXT_MSG), statistics (CMD_GET_STATS), and at least one of the optional features (telemetry, trace, path discovery).

Compliance requirements for The Protocol (the RF network layer) are defined in the companion document (Part 1: The Protocol, Appendix A).


Appendix B: Constants Summary

B.1. Size Constants

Constant Value Units
Default MAX_FRAME_SIZE 172 bytes

Size constants for The Protocol (public-key sizes, MTU, path limits, etc.) are defined in the companion document (Part 1, Appendix B).

B.2. Companion Protocol: Command Codes

Code Name
0x01 CMD_APP_START
0x02 CMD_SEND_TXT_MSG
0x03 CMD_SEND_CHANNEL_TXT_MSG
0x04 CMD_GET_CONTACTS
0x05 CMD_GET_DEVICE_TIME
0x06 CMD_SET_DEVICE_TIME
0x07 CMD_SEND_SELF_ADVERT
0x08 CMD_SET_ADVERT_NAME
0x09 CMD_ADD_UPDATE_CONTACT
0x0A CMD_SYNC_NEXT_MESSAGE
0x0B CMD_SET_RADIO_PARAMS
0x0C CMD_SET_RADIO_TX_POWER
0x0D CMD_RESET_PATH
0x0E CMD_SET_ADVERT_LATLON
0x0F CMD_REMOVE_CONTACT
0x10 CMD_SHARE_CONTACT
0x11 CMD_EXPORT_CONTACT
0x12 CMD_IMPORT_CONTACT
0x13 CMD_REBOOT
0x14 CMD_GET_BATT_AND_STORAGE
0x15 CMD_SET_TUNING_PARAMS
0x16 CMD_DEVICE_QUERY
0x17 CMD_EXPORT_PRIVATE_KEY
0x18 CMD_IMPORT_PRIVATE_KEY
0x19 CMD_SEND_RAW_DATA
0x1A CMD_SEND_LOGIN
0x1B CMD_SEND_STATUS_REQ
0x1C CMD_HAS_CONNECTION
0x1D CMD_LOGOUT
0x1E CMD_GET_CONTACT_BY_KEY
0x1F CMD_GET_CHANNEL
0x20 CMD_SET_CHANNEL
0x21 CMD_SIGN_START
0x22 CMD_SIGN_DATA
0x23 CMD_SIGN_FINISH
0x24 CMD_SEND_TRACE_PATH
0x25 CMD_SET_DEVICE_PIN
0x26 CMD_SET_OTHER_PARAMS
0x27 CMD_SEND_TELEMETRY_REQ
0x28 CMD_GET_CUSTOM_VARS
0x29 CMD_SET_CUSTOM_VAR
0x2A CMD_GET_ADVERT_PATH
0x2B CMD_GET_TUNING_PARAMS
0x32 CMD_SEND_BINARY_REQ
0x33 CMD_FACTORY_RESET
0x34 CMD_SEND_PATH_DISCOVERY_REQ
0x36 CMD_SET_FLOOD_SCOPE_KEY
0x37 CMD_SEND_CONTROL_DATA
0x38 CMD_GET_STATS
0x39 CMD_SEND_ANON_REQ
0x3A CMD_SET_AUTOADD_CONFIG
0x3B CMD_GET_AUTOADD_CONFIG
0x3C CMD_GET_ALLOWED_REPEAT_FREQ
0x3D CMD_SET_PATH_HASH_MODE
0x3E CMD_SEND_CHANNEL_DATA
0x3F CMD_SET_DEFAULT_FLOOD_SCOPE
0x40 CMD_GET_DEFAULT_FLOOD_SCOPE

Values 0x2C0x31 and 0x35 are gaps (unassigned / reserved).

B.3. Companion Protocol: Response Codes

See §2.6 for the full list (0x000x1C) and §2.7 for push codes (0x800x90).

Route types, payload types, advert node types, and text types used by The Protocol are tabulated in the companion document (Part 1, Appendix B).


Appendix C: Known Discrepancies Between Documentation and Implementation

This specification reflects the actual on-wire behaviour of a compliant implementation, not prior MeshCore documentation drafts. The following Companion Protocol discrepancies were identified and resolved in favour of the implementation:

C.1. Error Codes (§2.8)

The Core Companion Protocol reference document (companion_protocol.md) lists error codes such as:

These do not match the actual firmware implementation, which uses:

This specification uses the implementation’s values. The documentation’s values should be treated as obsolete.

C.2. Stats Frame via BLE vs. via CMD_GET_STATS

Two distinct stats mechanisms exist:

  1. CMD_GET_BATT_AND_STORAGE (0x14) returns PACKET_BATTERY (0x0C) with 11 bytes covering battery and storage.
  2. CMD_GET_STATS (0x38) returns PACKET_STATS (0x18) with a sub-type-tagged payload covering core, radio, or packet statistics.

Hosts SHOULD prefer CMD_GET_STATS for new development; CMD_GET_BATT_AND_STORAGE remains supported for legacy compatibility.

C.3. Stream-transport marker convention

Earlier drafts of this specification described 0x3E as the sole stream-transport start marker. The deployed reference firmware and at least two independent host implementations use the directional two-marker convention described in §2.1.2 (0x3C host-to-node, 0x3E node-to-host). Implementations that send 0x3E in both directions will fail to interoperate with nodes that validate the inbound marker. This specification uses the implementation’s convention.

C.4. CMD_SET_FLOOD_SCOPE renamed to CMD_SET_FLOOD_SCOPE_KEY

Earlier firmware and drafts of this specification referred to opcode 0x36 as CMD_SET_FLOOD_SCOPE. Current reference firmware renames it to CMD_SET_FLOOD_SCOPE_KEY to distinguish it from the newer CMD_SET_DEFAULT_FLOOD_SCOPE (0x3F), which configures a persistent default that applies when no current scope is set. The opcode 0x36 is unchanged; only the symbolic name has changed. Hosts targeting either name are on-the-wire compatible.

C.5. Send-command response codes (PACKET_OK vs. PACKET_SENT)

Earlier drafts of §2.5.6 listed all CMD_SEND_* commands as returning PACKET_SENT. The reference firmware distinguishes two classes of send:

Hosts implementing from earlier drafts that waited for PACKET_SENT on the fire-and-forget commands would have blocked indefinitely. This specification uses the implementation’s actual behaviour.

C.6. PACKET_SENT correlation field overloaded

The 4-byte field at offset 2 of PACKET_SENT was previously documented as a SHA-256-derived “expected ACK” value. In the reference firmware that interpretation applies only to CMD_SEND_TXT_MSG. For other PACKET_SENT-returning commands, the same offset carries either (a) a 4-byte pubkey-prefix slot used by the firmware to dispatch the eventual async push (login/status) or (b) a caller-chosen 4-byte request tag that the host uses to match the eventual push (trace, path-discovery, anon-request, binary-request, telemetry). See §2.6.2 for the per-command mapping. The field name expected_ack is retained for backwards compatibility but is a misnomer for the non-messaging cases.

C.7. Reserved-zero byte in CMD_SET_FLOOD_SCOPE_KEY and CMD_SET_PATH_HASH_MODE

Both commands require a second byte of 0x00 between the command code and the payload proper. Earlier drafts omitted this reserved byte from their field diagrams. The firmware validates it strictly: frames whose second byte is non-zero are silently ignored (no error response). Hosts implementing from the earlier diagrams will see their settings never take effect.

Discrepancies relating to The Protocol (advert flags byte, HMAC key length, TRACE path field, payload versioning) are documented in the companion document (Part 1, Appendix C).


End of Specification — Part 2

generated 2026-05-15 14:35 -0700 · Hugo