3/HOST

Host Layer Protocol (Companion Protocol)

Core Protocol Specification Part 2 - The Companion Protocol (Host Layer)

status
raw

Metadata

Detail Description
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 Network Protocol (RF Network Layer) used by nodes to communicate over a LoRa mesh is defined separately in 2/RF also referred to as Core Protocol Specification Part 1 - The Network Protocol (RF Network Layer) (Part 1). 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.

Preface

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.

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:

RoleUUID
Service6E400001-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 │
└──────────┴────────────┴──────────────┘
FieldSizeDescription
marker1Directional start marker (see below).
length2Unsigned 16-bit, little-endian. Length of payload only.
payloadNThe companion-protocol frame payload (§2.2).

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

MarkerHexASCIIMeaning
0x3C0x3C'<'Host → node (companion → radio).
0x3E0x3E'>'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:

DirectionCode rangeRoleDefined in
Host → node0x010x7FCommand code (CMD_*).§2.5
Node → host0x000x7FResponse packet code (PACKET_*).§2.6
Node → host0x800xFFPush 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:

VersionFeature Additions
1Baseline: session, contacts, text messages, channels, advert, signing, tuning.
2(historical — skipped in current firmware; reserved).
3PACKET_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.
5Telemetry permissions: CMD_SET_OTHER_PARAMS gains the packed telemetry_modes byte (env/loc/base fields); PACKET_SELF_INFO surfaces same.
7Multi-ACK support: PACKET_SELF_INFO gains multi_acks; login permissions include is_admin.
8Transport-scope support: CMD_SET_FLOOD_SCOPE_KEY (0x36), CMD_SEND_CONTROL_DATA (0x37), CMD_GET_STATS (0x38) + PACKET_STATS, PUSH_CODE_CONTROL_DATA (0x8E).
9PACKET_DEVICE_INFO gains repeat_enabled byte at offset 80.
10PACKET_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.
11CMD_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

CodeNameResponse
0x01CMD_APP_STARTPACKET_SELF_INFO
0x16CMD_DEVICE_QUERYPACKET_DEVICE_INFO
0x13CMD_REBOOT(none; connection drops)
0x33CMD_FACTORY_RESETPACKET_OK
0x14CMD_GET_BATT_AND_STORAGEPACKET_BATTERY
0x38CMD_GET_STATSPACKET_STATS
0x05CMD_GET_DEVICE_TIMEPACKET_CURR_TIME
0x06CMD_SET_DEVICE_TIMEPACKET_OK
0x25CMD_SET_DEVICE_PINPACKET_OK
0x1CCMD_HAS_CONNECTIONPACKET_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

CodeNameResponse
0x17CMD_EXPORT_PRIVATE_KEYPACKET_PRIVATE_KEY
0x18CMD_IMPORT_PRIVATE_KEYPACKET_OK
0x21CMD_SIGN_STARTPACKET_SIGN_START
0x22CMD_SIGN_DATAPACKET_OK
0x23CMD_SIGN_FINISHPACKET_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

CodeNameResponse
0x0BCMD_SET_RADIO_PARAMSPACKET_OK
0x0CCMD_SET_RADIO_TX_POWERPACKET_OK
0x15CMD_SET_TUNING_PARAMSPACKET_OK
0x2BCMD_GET_TUNING_PARAMSPACKET_TUNING_PARAMS
0x26CMD_SET_OTHER_PARAMSPACKET_OK
0x36CMD_SET_FLOOD_SCOPE_KEYPACKET_OK
0x3CCMD_GET_ALLOWED_REPEAT_FREQPACKET_ALLOWED_REPEAT_FREQ
0x3DCMD_SET_PATH_HASH_MODEPACKET_OK
0x3FCMD_SET_DEFAULT_FLOOD_SCOPEPACKET_OK
0x40CMD_GET_DEFAULT_FLOOD_SCOPEPACKET_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

CodeNameResponse
0x04CMD_GET_CONTACTSPACKET_CONTACT_STARTPACKET_CONTACT* → PACKET_CONTACT_END
0x1ECMD_GET_CONTACT_BY_KEYPACKET_CONTACT / PACKET_ERROR
0x09CMD_ADD_UPDATE_CONTACTPACKET_OK
0x0FCMD_REMOVE_CONTACTPACKET_OK
0x0DCMD_RESET_PATHPACKET_OK
0x10CMD_SHARE_CONTACTPACKET_OK
0x11CMD_EXPORT_CONTACTPACKET_EXPORT_CONTACT
0x12CMD_IMPORT_CONTACTPACKET_OK
0x3ACMD_SET_AUTOADD_CONFIGPACKET_OK
0x3BCMD_GET_AUTOADD_CONFIGPACKET_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:

BitMaskName
00x01Overwrite oldest when full
10x02Auto-add chat nodes
20x04Auto-add repeaters
30x08Auto-add room servers
40x10Auto-add sensors

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

2.5.5. Channels

CodeNameResponse
0x1FCMD_GET_CHANNELPACKET_CHANNEL_INFO
0x20CMD_SET_CHANNELPACKET_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

CodeNameResponse
0x02CMD_SEND_TXT_MSGPACKET_SENT
0x03CMD_SEND_CHANNEL_TXT_MSGPACKET_OK
0x3ECMD_SEND_CHANNEL_DATAPACKET_OK
0x19CMD_SEND_RAW_DATAPACKET_OK
0x32CMD_SEND_BINARY_REQPACKET_SENT
0x39CMD_SEND_ANON_REQPACKET_SENT
0x37CMD_SEND_CONTROL_DATAPACKET_OK
0x0ACMD_SYNC_NEXT_MESSAGEPACKET_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

CodeNameResponse
0x07CMD_SEND_SELF_ADVERTPACKET_OK
0x08CMD_SET_ADVERT_NAMEPACKET_OK
0x0ECMD_SET_ADVERT_LATLONPACKET_OK
0x2ACMD_GET_ADVERT_PATHPACKET_ADVERT_PATH
0x34CMD_SEND_PATH_DISCOVERY_REQPACKET_SENT
0x24CMD_SEND_TRACE_PATHPACKET_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)

CodeNameResponse
0x1ACMD_SEND_LOGINPUSH_CODE_LOGIN_SUCCESS / PUSH_CODE_LOGIN_FAIL
0x1BCMD_SEND_STATUS_REQPUSH_CODE_STATUS_RESPONSE
0x1DCMD_LOGOUTPACKET_OK
0x27CMD_SEND_TELEMETRY_REQPUSH_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

CodeNameResponse
0x28CMD_GET_CUSTOM_VARSPACKET_CUSTOM_VARS
0x29CMD_SET_CUSTOM_VARPACKET_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.

CodeNameDescription
0x00PACKET_OKCommand succeeded. Optional 4-byte value follows.
0x01PACKET_ERRORCommand failed. Optional error code follows.
0x02PACKET_CONTACT_STARTStart of contact enumeration; 4-byte total count.
0x03PACKET_CONTACTOne contact record. See §2.6.18.
0x04PACKET_CONTACT_ENDEnd of contact enumeration.
0x05PACKET_SELF_INFONode’s self information. See §2.6.3.
0x06PACKET_SENTMessage transmission scheduled. See §2.6.2.
0x07PACKET_CONTACT_MSG_RECVReceived contact message (legacy, pre-v3). See §2.6.6.
0x08PACKET_CHANNEL_MSG_RECVReceived channel message (legacy, pre-v3). See §2.6.7.
0x09PACKET_CURR_TIMECurrent device time. See §2.6.10.
0x0APACKET_NO_MORE_MSGSNo queued messages.
0x0BPACKET_EXPORT_CONTACTExported contact blob.
0x0CPACKET_BATTERYBattery and storage. See §2.6.5.
0x0DPACKET_DEVICE_INFODevice information. See §2.6.4.
0x0EPACKET_PRIVATE_KEYExported private key.
0x0FPACKET_DISABLEDFeature not enabled.
0x10PACKET_CONTACT_MSG_V3Received contact message (v3, with SNR). See §2.6.6.
0x11PACKET_CHANNEL_MSG_V3Received channel message (v3, with SNR). See §2.6.7.
0x12PACKET_CHANNEL_INFOChannel slot info. See §2.6.9.
0x13PACKET_SIGN_STARTSigning session established. See §2.6.14.
0x14PACKET_SIGNATURESigning session signature result. See §2.6.15.
0x15PACKET_CUSTOM_VARSCustom variable listing. See §2.6.16.
0x16PACKET_ADVERT_PATHCached advert path. See §2.6.17.
0x17PACKET_TUNING_PARAMSRadio tuning parameters. See §2.6.13.
0x18PACKET_STATSStatistics response. See §2.6.8.
0x19PACKET_AUTOADD_CONFIGAuto-add configuration.
0x1APACKET_ALLOWED_REPEAT_FREQAllowed client-repeat frequency ranges. See §2.6.19.
0x1BPACKET_CHANNEL_DATA_RECVReceived channel datagram (non-text). See §2.6.11.
0x1CPACKET_DEFAULT_FLOOD_SCOPEStored 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       │
└──────┴─────────────┴───────────────────────────┴───────────────────┘
OffsetSizeFieldDescription
01code0x06
11send_method0x00 = sent via direct (known path); 0x01 = sent via flood. Always 0x00 for CMD_SEND_TRACE_PATH.
24expected_ack or tagPer-command correlation handle (see table below), 4-byte little-endian.
64est_timeout_msNode’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 commandField semantics
CMD_SEND_TXT_MSGexpected_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_LOGINexpected_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_REQexpected_ack — first 4 bytes of the recipient’s public key (legacy scheme; matches the eventual PUSH_CODE_STATUS_RESPONSE).
CMD_SEND_ANON_REQtag — caller-chosen request identifier, echoed in the eventual response push.
CMD_SEND_TELEMETRY_REQtag — caller-chosen request identifier, echoed in PUSH_CODE_TELEMETRY_RESPONSE.
CMD_SEND_BINARY_REQtag — caller-chosen request identifier, echoed in PUSH_CODE_BINARY_RESPONSE.
CMD_SEND_PATH_DISCOVERY_REQtag — caller-chosen request identifier, echoed in PUSH_CODE_PATH_DISCOVERY_RESP.
CMD_SEND_TRACE_PATHtag — 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   │        │              │              │     │      │      │
└──────┴──────────┴──────────┴──────────────┴───────────┴────────────┴────────────┴─────────────┴─────────────────┴───────────────────┴───────┴────────┴──────────────┴──────────────┴─────┴─────┴──────┘
OffsetSizeFieldDescription
01code0x05
11adv_typeNode’s ADV_TYPE_* value.
21tx_powerCurrent TX power (dBm).
31max_tx_powerMaximum allowed TX power (dBm).
432pub_keyNode’s Ed25519 public key.
364adv_latAdvertised latitude. degrees × 1,000,000, signed LE.
404adv_lonAdvertised longitude. degrees × 1,000,000, signed LE.
441multi_acksMulti-ACK count.
451adv_loc_policyAdvert location policy (implementation-defined).
461telemetry_modePacked: bits 4–5 = env, 2–3 = loc, 0–1 = base.
471manual_add_contactsBoolean (0 or non-zero).
484radio_freqFrequency in kHz × 1000 (i.e., Hz). Divide by 1000 for kHz.
524radio_bwBandwidth in kHz × 1000 (i.e., Hz).
561radio_sfSpreading factor (5–12).
571radio_crCoding rate (5–8).
58+varnameUTF-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        │
└──────┴─────────┴──────────────┴──────────────┴─────────┴──────────────┴─────────┴──────────┴────────────┴──────────────────┘
OffsetSizeFieldDescription
01code0x0D
11fw_verCompanion 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.
21max_contactsMax contacts × 2 (multiply by 2 to get real value).
31max_channelsMax channel slots.
44ble_pinBLE bonding PIN (0 = disabled). LE uint32.
812fw_buildFirmware build string, null-padded UTF-8.
2040modelHardware model string, null-padded UTF-8.
6020versionFirmware version string, null-padded UTF-8.
801repeat_enabled(present if fw_ver ≥ 9) Client-repeat mode.
811path_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    │
└──────┴──────────────┴─────────────┴──────────────┘
OffsetSizeFieldDescription
01code0x0C
12battery_mvBattery voltage in millivolts, uint16 LE.
34used_kbUsed storage in kilobytes, uint32 LE.
74total_kbTotal 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 │
└──────┴──────┴──────────┴─────────────┴──────────┴───────────┴──────────┴──────────┘
OffsetSizeFieldDescription
01code0x1B
11snrSigned 8-bit, SNR_dB × 4 (0.25 dB resolution).
22reservedEmitted as zero; hosts MUST ignore.
41channel_idxSlot index of the channel the datagram arrived on.
51path_lenEncoded 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).
62data_typeApplication-defined 16-bit datagram sub-type, little-endian. Matches the data_type the sender passed to CMD_SEND_CHANNEL_DATA.
81data_lenLength of payload in bytes.
9varpayloadUp 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.

CodeNameData
0x80PUSH_CODE_ADVERTAdvertisement received (pubkey prefix).
0x81PUSH_CODE_PATH_UPDATEDA contact’s path was updated (pubkey prefix).
0x82PUSH_CODE_SEND_CONFIRMEDAn outgoing message was ACKed (ACK checksum).
0x83PUSH_CODE_MSG_WAITINGOne or more messages are queued; host should call CMD_SYNC_NEXT_MESSAGE.
0x84PUSH_CODE_RAW_DATARaw custom packet received.
0x85PUSH_CODE_LOGIN_SUCCESSLogin attempt succeeded.
0x86PUSH_CODE_LOGIN_FAILLogin attempt failed.
0x87PUSH_CODE_STATUS_RESPONSEStatus request response.
0x88PUSH_CODE_LOG_RX_DATARaw RF log data (SNR, RSSI, bytes).
0x89PUSH_CODE_TRACE_DATATrace route response.
0x8APUSH_CODE_NEW_ADVERTAdvert from an unknown (new) node.
0x8BPUSH_CODE_TELEMETRY_RESPONSETelemetry response (CayenneLPP).
0x8CPUSH_CODE_BINARY_RESPONSEBinary request response.
0x8DPUSH_CODE_PATH_DISCOVERY_RESPPath discovery response.
0x8EPUSH_CODE_CONTROL_DATAControl data packet received (e.g., DISCOVER_RESP).
0x8FPUSH_CODE_CONTACT_DELETEDA contact was deleted (pubkey prefix).
0x90PUSH_CODE_CONTACTS_FULLContact 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.

CodeNameMeaning
0x01ERR_UNSUPPORTEDThe command or a requested feature is not supported.
0x02ERR_NOT_FOUNDThe referenced item (contact, channel slot) does not exist.
0x03ERR_TABLE_FULLThe target table (contacts, channels) is full.
0x04ERR_BAD_STATEThe node is in a state that forbids this command (e.g., contact iteration in progress).
0x05ERR_FILE_IOA persistent-storage operation failed (e.g., filesystem format during CMD_FACTORY_RESET).
0x06ERR_ILLEGAL_ARGA 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

ConstantValueUnits
Default MAX_FRAME_SIZE172bytes

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

CodeName
0x01CMD_APP_START
0x02CMD_SEND_TXT_MSG
0x03CMD_SEND_CHANNEL_TXT_MSG
0x04CMD_GET_CONTACTS
0x05CMD_GET_DEVICE_TIME
0x06CMD_SET_DEVICE_TIME
0x07CMD_SEND_SELF_ADVERT
0x08CMD_SET_ADVERT_NAME
0x09CMD_ADD_UPDATE_CONTACT
0x0ACMD_SYNC_NEXT_MESSAGE
0x0BCMD_SET_RADIO_PARAMS
0x0CCMD_SET_RADIO_TX_POWER
0x0DCMD_RESET_PATH
0x0ECMD_SET_ADVERT_LATLON
0x0FCMD_REMOVE_CONTACT
0x10CMD_SHARE_CONTACT
0x11CMD_EXPORT_CONTACT
0x12CMD_IMPORT_CONTACT
0x13CMD_REBOOT
0x14CMD_GET_BATT_AND_STORAGE
0x15CMD_SET_TUNING_PARAMS
0x16CMD_DEVICE_QUERY
0x17CMD_EXPORT_PRIVATE_KEY
0x18CMD_IMPORT_PRIVATE_KEY
0x19CMD_SEND_RAW_DATA
0x1ACMD_SEND_LOGIN
0x1BCMD_SEND_STATUS_REQ
0x1CCMD_HAS_CONNECTION
0x1DCMD_LOGOUT
0x1ECMD_GET_CONTACT_BY_KEY
0x1FCMD_GET_CHANNEL
0x20CMD_SET_CHANNEL
0x21CMD_SIGN_START
0x22CMD_SIGN_DATA
0x23CMD_SIGN_FINISH
0x24CMD_SEND_TRACE_PATH
0x25CMD_SET_DEVICE_PIN
0x26CMD_SET_OTHER_PARAMS
0x27CMD_SEND_TELEMETRY_REQ
0x28CMD_GET_CUSTOM_VARS
0x29CMD_SET_CUSTOM_VAR
0x2ACMD_GET_ADVERT_PATH
0x2BCMD_GET_TUNING_PARAMS
0x32CMD_SEND_BINARY_REQ
0x33CMD_FACTORY_RESET
0x34CMD_SEND_PATH_DISCOVERY_REQ
0x36CMD_SET_FLOOD_SCOPE_KEY
0x37CMD_SEND_CONTROL_DATA
0x38CMD_GET_STATS
0x39CMD_SEND_ANON_REQ
0x3ACMD_SET_AUTOADD_CONFIG
0x3BCMD_GET_AUTOADD_CONFIG
0x3CCMD_GET_ALLOWED_REPEAT_FREQ
0x3DCMD_SET_PATH_HASH_MODE
0x3ECMD_SEND_CHANNEL_DATA
0x3FCMD_SET_DEFAULT_FLOOD_SCOPE
0x40CMD_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).