3/HOST
Host Layer Protocol (Companion Protocol)
Core Protocol Specification Part 2 - The Companion Protocol (Host Layer)
- Status:raw
- Lead(s):enot
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
0xprefix denotes hexadecimal. Example:0x80= 128 decimal.0bprefix denotes binary. Example:0b0110= 6 decimal.- Bit positions count from the least-significant bit as bit 0. A byte
0bABCDEFGHhasAas bit 7 andHas bit 0. - Byte ranges are inclusive on both ends. “bytes 4–7” means four bytes at offsets 4, 5, 6, 7.
- Field sizes are expressed in bytes unless noted otherwise.
1.2. Byte Order
- All multi-byte integer fields are little-endian unless explicitly stated otherwise.
- Cryptographic outputs (hashes, signatures, MACs, public keys) are byte sequences and are not re-ordered — they are transmitted in the order produced by the underlying algorithm.
- The CayenneLPP format used for sensor data is the exception: it is big-endian (per the CayenneLPP specification).
1.3. Strings
- All textual fields (names, messages, passwords) are UTF-8 encoded.
- Fixed-size text fields are padded with null bytes (
0x00) on the right when shorter than the field size. - Variable-length text fields at the end of a payload are not null-terminated on the wire; their length is implied by the remaining payload length.
1.4. Terminology
- Node — any device participating in the mesh.
- Contact — a known peer node, identified by its Ed25519 public key.
- Advert — an advertisement packet broadcast by a node to announce its presence.
- Path — an ordered list of node hashes representing a route through the mesh.
- Node hash — a short prefix of a node’s public key used for routing. One, two, or three bytes; the length is selected per-packet.
- Channel hash — a one-byte identifier for a group channel, derived from the channel secret.
- Direct routing — the packet travels along a pre-known path, hop-by-hop.
- Flood routing — the packet is rebroadcast by every node that hears it (with deduplication).
- Transport codes — optional 4-byte region/scope identifier that filters which nodes will rebroadcast.
- MAC — a 2-byte HMAC-SHA256 prefix used as a message authentication code.
- MTU — Maximum Transmission Unit; the largest physical-layer payload the radio can carry.
- Companion — a host application (phone, PC, embedded controller) that manages a node over a local link.
- Push notification (or “push”) — an unsolicited message from the node to the companion.
- Companion Protocol Version (
companion_proto_ver) — a monotonically-increasing integer identifying the capability level of this protocol. The node reports its supported version inPACKET_DEVICE_INFO.fw_ver(§2.6.4); the host declares its own capability level inCMD_DEVICE_QUERY.app_target_ver(§2.5). Both sides are expected to emit and accept the least common version. This is not the same as the firmware release version (e.g., “v1.15.0”) or build date; those are separate strings also carried inPACKET_DEVICE_INFO. The current version at the time of this specification is 11. Thefw_verfield name is retained on the wire for backwards compatibility but is a misnomer — it describes the Companion Protocol capability level, not the firmware release. - Firmware Version String (
FIRMWARE_VERSION) — the human-readable version identifier of the node firmware (e.g.,"v1.15.0"), distinct from the Companion Protocol Version. Carried inPACKET_DEVICE_INFO.version. - Firmware Build Date (
FIRMWARE_BUILD_DATE) — a short free-form string identifying when the firmware was built (e.g.,"19 Apr 2026"). Carried inPACKET_DEVICE_INFO.fw_build.
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:
- The host scans for BLE devices advertising the service UUID.
- The host connects, discovers the service and characteristics, and enables notifications on the TX characteristic.
- The host writes one command frame per BLE write on the RX characteristic.
- 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 | 0x01–0x7F | Command code (CMD_*). | §2.5 |
| Node → host | 0x00–0x7F | Response packet code (PACKET_*). | §2.6 |
| Node → host | 0x80–0xFF | 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_SIZE — 172 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:
- A host command is answered by at most one response frame from the node, delivered synchronously before the node processes the next command. A command that produces no response (
CMD_REBOOT) is explicitly called out where it occurs. - Push notifications are unsolicited and asynchronous. They may arrive interleaved between a command and its response, but a response frame is always emitted before the next command is processed.
- The node may queue frames (
addToOfflineQueue) while the host is not connected and emit them in order when the host reconnects and begins draining the queue withCMD_SYNC_NEXT_MESSAGE.
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:
- Connect (transport-specific).
- Enable notifications (BLE only).
CMD_APP_START— identifies the host to the node, receivesPACKET_SELF_INFO.CMD_DEVICE_QUERY— negotiates protocol version, receivesPACKET_DEVICE_INFO.CMD_SET_DEVICE_TIME— sets the node’s real-time clock to the host’s current time.CMD_GET_CONTACTS— enumerates contacts.CMD_GET_CHANNEL— for each channel slot the node supports, enumerate channels.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 │
└──────┴──────────┴──────────┘
- Bytes 1–7: reserved; senders SHOULD set to zero.
- Bytes 8+: optional UTF-8 application name (informational).
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:
0x00STATS_TYPE_CORE— battery, uptime, errors, queue length.0x01STATS_TYPE_RADIO— noise floor, RSSI, SNR, airtime counters.0x02STATS_TYPE_PACKETS— packet counters.
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:
- The host sends
CMD_SIGN_START(no payload) to begin a new session. The node allocates a buffer of up toMAX_SIGN_DATA_LENbytes (8192 in the reference firmware) and replies withPACKET_SIGN_START(§2.6.14) carrying that maximum. - The host sends one or more
CMD_SIGN_DATAframes, each carrying a chunk of bytes to append. The combined size of all chunks across a single session MUST NOT exceed themax_lenreported byPACKET_SIGN_START; an over-sized chunk is rejected withPACKET_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. - The host sends
CMD_SIGN_FINISH(no payload). The node computes the Ed25519 signature over the concatenated chunks, deallocates the buffer, and replies withPACKET_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 │
└──────┴──────────────────┘
chunk— raw bytes to append. The frame’s total length, minus 1 for the code byte, determines the chunk size. All bytes ofchunkare appended; there is no internal framing or length prefix.
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 │
└──────┴──────────┴──────────┴─────┴─────┘
freq— carrier frequency in kHz, 32-bit little-endian. Example:869618→ 869.618 MHz.bw— bandwidth in kHz, 32-bit little-endian. Example:250→ 250 kHz.sf— spreading factor (5–12).cr— coding rate (5–8, representing 4/5 through 4/8).
CMD_SET_RADIO_TX_POWER — sets the LoRa transmit power.
┌──────┬────────────┐
│ 0x0C │ tx_power │
│ 1 B │ 1 B int8 │
└──────┴────────────┘
tx_power— signed 8-bit dBm value. The valid range is−9through a board-specific maximum (MAX_LORA_TX_POWER, typically 20 or 22 dBm). Out-of-range values are rejected withPACKET_ERROR+ERR_ILLEGAL_ARG. The upper bound is also reported inPACKET_SELF_INFO.max_tx_power.
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 │
└──────┴──────────────────┴──────────────────┘
rx_delay_base— 32-bit little-endian unsigned integer, transmitted asvalue × 1000. Stored internally as a float whose units are seconds; e.g., transmit1500to request a base receive delay of 1.500 seconds.airtime_factor— 32-bit little-endian unsigned integer, same× 1000scaling. Multiplies the modem’s airtime estimate to produce per-hop retransmit delays.
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)│
└──────┴─────────────────────┴──────────────────┴──────────────────┴─────────────┘
manual_add_contacts— boolean (0x00/ non-zero). When non-zero, adverts from unknown nodes do not auto-add to contacts even ifCMD_SET_AUTOADD_CONFIGwould otherwise permit it.telemetry_modes— packed telemetry permissions, same encoding asPACKET_SELF_INFO.telemetry_mode:- Bits 0–1:
telemetry_mode_base— base telemetry permission (0 = deny, 1 = allow-per-flags, 2 = allow-all). - Bits 2–3:
telemetry_mode_loc— location telemetry permission, same enumeration. - Bits 4–5:
telemetry_mode_env— environmental telemetry permission, same enumeration. - Bits 6–7: reserved.
- Bits 0–1:
advert_loc_policy— one ofADVERT_LOC_NONE(0x00) orADVERT_LOC_SHARE(0x01). Implementation-specific values beyond these are reserved.multi_acks— unsigned 8-bit count of additional ACK retransmissions to emit after the primary ACK (for increased delivery robustness at the cost of airtime).
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 │
└──────┴──────────┴──────────────────┘
reserved— MUST be0x00. Frames whose second byte is non-zero are rejected.transport_key— 16-byte transport key. When thetransport_keyfield is omitted (frame length is exactly 2), the current scope is cleared.
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 │
└──────┴──────────┴──────┘
reserved— MUST be0x00. Frames whose second byte is non-zero are rejected.mode—0,1, or2(1, 2, or 3 bytes per hash). Values ≥ 3 are rejected withPACKET_ERROR+ERR_ILLEGAL_ARG.
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 │
└──────┴──────────────────┴──────────────────┘
scope_name— 31 bytes, UTF-8, null-padded on the right. Must be a non-empty string (length 1–30). An empty name is rejected withPACKET_ERROR+ERR_ILLEGAL_ARG.transport_key— 16-byte transport key.
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_START → PACKET_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)
pub_key— 32-byte Ed25519 public key.type—ADV_TYPE_*value.flags— contact flags (application-defined; typically includes “favourite”, “block”, etc.).out_path_len— 1-byte path-length field (same encoding as Part 1, §2.5). Encodes both the per-hash size and the hop count.out_path— always 64 bytes on the wire (fullMAX_PATH_SIZE), regardless of the actual hop count. The meaningful byte length ishop_count × hash_sizeas derived fromout_path_len; trailing bytes are unused and MUST be transmitted as zero-padding so that the frame is size-stable.name— 32 bytes, null-padded UTF-8.last_advert_timestamp— 4-byte Unix time.- Optional
gps_lat,gps_lon— each 4-byte signed int,degrees × 1e6. Present as a pair or not at all. - Optional
lastmod— 4-byte Unix time of last modification. Only valid when the location pair precedes it. If omitted, the node uses its current RTC time as the effectivelastmod.
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 │
└──────┴─────────────┴───────┴──────────┘
channel_idx— slot index (0 tomax_channels - 1).name— 32-byte null-padded UTF-8 name.secret— 16-byte channel secret. All-zero deletes the slot. The 32-byte secret variant is not supported via this command.
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 │
└──────┴──────────┴──────────┴───────────┴─────────────────┴──────────────┘
txt_type—TXT_TYPE_*value (see Part 1, §2.9.4).attempt— retry counter (0–3).timestamp— Unix time (4 B LE).pub_key_prefix— first 6 bytes of the destination contact’s Ed25519 public key. This is used as a local lookup key into the node’s contact table; it does not appear on the RF wire (the RF-layer source/destination hashes are derived separately — see Part 1, §2.9). If the 6-byte prefix matches more than one contact in the node’s table, the outcome is implementation-defined; hosts SHOULD avoid this situation by checking the contact table before sending.message— UTF-8 message text, up toMAX_TEXT_LEN= 160 bytes.
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_keyas 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 │
└──────┴─────────────┴──────────┴──────────────┴───────────┴──────────┘
channel_idx— slot index of the channel to send on.path_len— 1-byte path-length field (same encoding as Part 1, §2.5). The sentinel value0xFFindicates a flood-routed send with no pre-known path; in that case, thepathfield is omitted.path—hop_count × hash_sizebytes as derived frompath_len. Omitted entirely whenpath_len == 0xFF.data_type— 2-byte little-endian datagram sub-type.0x0000is reserved and MUST NOT be used.0xFFFFis the developer namespace. All other values are application-defined.payload— up toMAX_CHANNEL_DATA_LENGTHbytes (=MAX_FRAME_SIZE - 9= 163 bytes) of application-defined content, encrypted on the wire by the node using the channel secret (see Part 1, §2.11).
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 │
└──────┴──────────────────┘
name— UTF-8 text without null terminator. The node stores the firstmin(len − 1, 31)bytes and null-terminates internally; longer names are silently truncated.
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 │
└──────┴──────────┴─────────────┘
reserved— currently unused; hosts SHOULD send0x00.pub_key— 32-byte Ed25519 public key of the peer whose path is being queried. The node matches by the first 6 bytes of the key against its cache.
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 │
└──────┴──────────┴─────────────┘
reserved— MUST be0x00. Frames whose second byte is non-zero are silently ignored.pub_key— 32-byte Ed25519 public key of the contact to probe.
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 │
└──────┴─────┴──────────┴───────┴──────────────┘
tag— 4-byte caller-chosen request identifier (little-endian), echoed in the eventualPUSH_CODE_TRACE_DATA.auth_code— 4-byte application-defined authentication code, copied verbatim into the TRACE payload.flags— TRACE flags byte. Low 2 bits encode the path hash size (1 << bitsbytes per hash; see Part 1, §2.13). Upper bits reserved, SHOULD be zero.path_hashes— the sequence of hop hashes (hop_count × hash_sizebytes). The frame’s total length minus 10 gives the byte length of this field. The byte length MUST be an integer multiple of the per-hash size; otherwise the frame is rejected withPACKET_ERROR+ERR_ILLEGAL_ARG.
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 │
└──────┴────────────────────────────────┘
- The payload is an ASCII string of the form
name:value, with no leading or trailing whitespace and no null terminator. The first:byte (offset ≥ 1 within the payload) is the separator; everything before is the variable name, everything after is its value. - A payload lacking a
:separator, or with an empty name, is rejected withPACKET_ERROR+ERR_ILLEGAL_ARG. - A variable name the node does not recognise is rejected with
PACKET_ERROR+ERR_ILLEGAL_ARG.
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 │
└──────┴──────┴──────────┴──────────────┴──────────┴──────────┴───────────┴──────────────────┴───────────┘
snr— signed 8-bit,SNR_dB × 4(0.25 dB resolution).pubkey_prefix— 6 bytes of sender’s public key.path_len— encoded path length (see Part 1, §2.5) of the path the packet traversed when arriving via flood routing. The sentinel value0xFFindicates the packet arrived via direct routing (no path was recorded); hosts MUST check for this sentinel before decodingpath_lenas a standard §2.5 encoded path length. Nopathbytes follow this field in either case;path_lenis conveyed purely for host-side display/logging.txt_type— as Part 1, §2.9.4.signature— present only whentxt_type == 2(signed plain); 4-byte prefix of sender’s public key.message— UTF-8 text.
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 │
└──────┴──────┴──────────┴─────────────┴──────────┴──────────┴───────────┴───────────┘
snr(V3 only) — signed 8-bit,SNR_dB × 4(0.25 dB resolution).reserved(V3 only) — 2 bytes, emitted as zero.channel_idx— slot index of the channel the message arrived on.path_len— encoded path length (see Part 1, §2.5) when flood-routed, or the sentinel0xFFwhen direct-routed. Same semantics asPACKET_CONTACT_MSG_RECV.path_len.txt_type— as Part 1, §2.9.4.timestamp— sender’s Unix time, 4 B LE.message— UTF-8 text. The convention for channel messages is"sender_name: body", so the receiver can display who sent it (see Part 1, §2.11.1).
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) │
└──────┴───────────┴─────────────┴──────────┴──────────┴─────────────┴─────────────┘
snrisSNR_dB × 4(divide by 4.0 to get dB).
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 │
└──────┴──────────────────┴──────────────────┘
scope_name— 31 bytes, UTF-8, null-padded on the right.transport_key— 16-byte transport key.
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 │
└──────┴──────────────────┴──────────────────┘
rx_delay_base— 32-bit little-endian unsigned integer, encoded asvalue × 1000. Divide by 1000.0 to obtain seconds.airtime_factor— 32-bit little-endian unsigned integer, same scaling. Unitless multiplier; see §2.5.3CMD_SET_TUNING_PARAMS.
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 │
└──────┴──────────┴──────────────────┘
reserved— emitted as zero; hosts MUST ignore.max_len— 32-bit little-endian unsigned integer, the maximum number of data bytes the node will accept across the subsequentCMD_SIGN_DATAsequence (typically 8192).
2.6.15. PACKET_SIGNATURE
Returned in response to CMD_SIGN_FINISH. Total frame length is 65 bytes.
┌──────┬─────────────────┐
│ 0x14 │ signature │
│ 1 B │ 64 bytes │
└──────┴─────────────────┘
signature— 64-byte Ed25519 signature over the concatenated contents of all precedingCMD_SIGN_DATAchunks in this session.
2.6.16. PACKET_CUSTOM_VARS
Returned in response to CMD_GET_CUSTOM_VARS.
┌──────┬──────────────────────────────────────────────────────────┐
│ 0x15 │ "name1:value1,name2:value2,..." (ASCII) │
│ 1 B │ variable │
└──────┴──────────────────────────────────────────────────────────┘
- The payload is an ASCII string of
:-separated name/value pairs, joined by a single,byte between consecutive pairs. There is no leading or trailing punctuation and no null terminator. The overall content is bounded to roughly 140 bytes to fit withinMAX_FRAME_SIZE. - A node with no custom variables returns a frame containing only the code byte (1-byte total). A host parser that splits on
,and then:MUST handle both an empty payload and payloads where a final pair has no trailing,.
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 │
└──────┴─────────────────┴──────────┴──────────┘
recv_timestamp— 32-bit Unix time (seconds) at which the cached path was last observed.path_len— encoded path length (see Part 1, §2.5). Includes the hop-count and per-hash-size encoding.path—hop_count × hash_sizebytes.
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:
PACKET_CONTACT(0x03) — one record in aCMD_GET_CONTACTSenumeration, or the response toCMD_GET_CONTACT_BY_KEY.PUSH_CODE_NEW_ADVERT(0x8A) — see §2.7.
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 │
└──────┴──────────────────────────────────────────────────────┘
- Each range is an 8-byte tuple of
lower_freqandupper_freq, both unsigned 32-bit little-endian integers in kHz. - The number of ranges
Nis implicit from the frame length:N = (frame_length − 1) / 8. - A frame containing only the code byte indicates no ranges are permitted; the node is not acting as a client-repeater on any frequency.
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 │
└──────┴─────────────┘
pub_key— full Ed25519 public key of the peer whose advert was received.
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 │
└──────┴───────────┴─────────────────┘
ack_hash— 4-byte ACK hash (first 4 bytes of SHA-256 over the original message; see Part 1, §2.12). Hosts match this against theexpected_ackfield from the precedingPACKET_SENTto identify which outgoing message was ACKed.trip_time_ms— 32-bit little-endian unsigned integer, round-trip latency in milliseconds as measured by the node from send time to ACK receipt.
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 │
└──────┴──────┴──────┴──────────┴──────────┘
snr— signed 8-bit,SNR_dB × 4of the received packet.rssi— signed 8-bit, RSSI in dBm.reserved— emitted as0xFF; hosts MUST ignore. (A future protocol version may repurpose this byte for path-length metadata.)payload— the raw packet payload, passed to the host unmodified. Interpretation is application-specific.
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 │
└──────┴─────────────┴─────────────────┘
is_admin— always zero for legacy servers.pubkey_prefix— first 6 bytes of the remote contact’s public key.
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 │
└──────┴─────────────┴─────────────────┴──────────────────┴──────────────────┴──────────────────┘
permissions— server-supplied permission flags (bit 0 = admin, other bits server-defined).pubkey_prefix— first 6 bytes of the remote contact’s public key.server_timestamp— 32-bit little-endian Unix time reported by the server at login.acl_permissions— server-supplied ACL permission bitmap; application-defined.fw_ver_level— the server’s own firmware/protocol version byte.
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 │
└──────┴──────────┴─────────────────┘
reserved— emitted as zero; hosts MUST ignore.pubkey_prefix— first 6 bytes of the attempted login target’s public key.
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 │
└──────┴──────────┴─────────────────┴──────────────────┘
reserved— emitted as zero; hosts MUST ignore.pubkey_prefix— first 6 bytes of the responding peer’s public key.status_data— application-defined status payload returned by the peer (opaque to the protocol).
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 │
└──────┴──────┴──────┴──────────────┘
snr— signed 8-bit,SNR_dB × 4.rssi— signed 8-bit, RSSI in dBm.raw_packet— the received over-the-air bytes, up toMAX_TRANS_UNITin length. Hosts that don’t log raw traffic MAY ignore this frame.
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 │
└──────┴──────────┴──────────┴───────┴─────┴──────────┴─────────────┴──────────┴────────────┘
reserved— emitted as zero.path_len— the raw byte count of the path hash array; NOT the encodedpath_lengthfield used elsewhere.flags— TRACE flags byte (low 2 bits encode the per-hash size as1 << bits; see Part 1, §2.13). Upper 6 bits reserved.tag— 4-byte request identifier (little-endian), echoing thetagfrom the originatingCMD_SEND_TRACE_PATH.auth_code— 4-byte authentication code (little-endian).path_hashes—path_lenbytes of the original hop-hash sequence the TRACE was sent along.path_snrs—path_len >> path_szSNR bytes (one per hop), each a signed 8-bitSNR_dB × 4value, in traversal order.final_snr— signed 8-bit SNR of the packet as received by this node (i.e., the last hop, measured here).
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 │
└──────┴──────────┴─────────────────┴──────────────────────┘
reserved— emitted as zero.pubkey_prefix— first 6 bytes of the responding peer’s public key.cayenne_lpp_data— telemetry data in CayenneLPP format (big-endian, per §1.2).
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 │
└──────┴──────────┴──────────┴──────────────────────┘
reserved— emitted as zero.tag— 4-byte little-endian identifier, matching thetagfrom the originatingPACKET_SENT.response_data— application-defined payload returned by the peer.
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 │
└──────┴──────────┴─────────────────┴───────────────┴──────────┴──────────────┴──────────┘
reserved— emitted as zero.pubkey_prefix— first 6 bytes of the responding peer’s public key.out_path_len/out_path— encoded path length (see Part 1, §2.5) andhop_count × hash_sizebytes describing the outbound path from this node to the peer.in_path_len/in_path— same encoding, describing the inbound path from the peer back to this node. The two paths are usually — but not necessarily — reverses of each other.
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 │
└──────┴──────┴──────┴──────────┴──────────┘
snr— signed 8-bit,SNR_dB × 4.rssi— signed 8-bit, RSSI in dBm.path_len— raw byte count of the RF-layer path field. For a zero-hop control packet this is always zero; the field is retained for forward compatibility.payload— the control packet payload, beginning with its sub-type/flags byte (see Part 1, §2.15).
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 │
└──────┴─────────────┘
pub_key— full Ed25519 public key of the contact that was deleted. The host SHOULD remove any locally-cached state for this contact.
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:
- Deliver exactly one frame per transport-level write or notification (§2.1.4).
- Implement the initialization sequence in §2.4.
- 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. - Respond with
PACKET_ERROR+ERR_UNSUPPORTEDto unknown command codes. - Emit
PUSH_CODE_MSG_WAITINGwhen queued messages are available. - 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.
- If the implementation supports a stream transport, emit the directional marker matching its role (
0x3Efrom a node,0x3Cfrom 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 0x2C–0x31 and 0x35 are gaps (unassigned / reserved).
B.3. Companion Protocol: Response Codes
See §2.6 for the full list (0x00–0x1C) and §2.7 for push codes (0x80–0x90).
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:
0x03— “Channel not found”0x04— “Channel already exists”0x05— “Channel index out of range”0x06— “Secret mismatch”0x07— “Message too long”0x08— “Device busy”0x09— “Not enough storage”
These do not match the actual firmware implementation, which uses:
0x01ERR_UNSUPPORTED0x02ERR_NOT_FOUND0x03ERR_TABLE_FULL0x04ERR_BAD_STATE0x05ERR_FILE_IO0x06ERR_ILLEGAL_ARG
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:
CMD_GET_BATT_AND_STORAGE(0x14) returnsPACKET_BATTERY(0x0C) with 11 bytes covering battery and storage.CMD_GET_STATS(0x38) returnsPACKET_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:
- Sends that kick off an over-the-air exchange which the host will later correlate with a response or ACK (
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) returnPACKET_SENTwith a 4-byte correlation handle. - Sends that are “fire-and-forget” at the host level (
CMD_SEND_CHANNEL_TXT_MSG,CMD_SEND_CHANNEL_DATA,CMD_SEND_RAW_DATA,CMD_SEND_CONTROL_DATA) returnPACKET_OKwith no payload.
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).