release: v0.1.0

First cut release. Snapshots ./src/ as ./versions/v0.1.0/ via
`just cut-version v0.1.0` (22 files: 21 chapter MDs + SUMMARY.md).
Deployed alongside `latest` at:

  https://swaits.github.io/meshcore-spec/v0.1.0/

Also:

- CHANGELOG.md adopts Keep a Changelog-style version sections.
  Future entries land under [Unreleased] and graduate to a version
  heading when cut. The existing date-stamped pass entries stay
  put as a historical record of the work captured in v0.1.0.
- README.md fixes a stale "per vX.Y.Z tag" mention left over from
  the tag-based versioning model, and links the current release
  explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.7 committed 2026-05-12 10:04:40 -06:00
1 parent 74864761fa
commit 44305a06db
24 files changed
+2478 -2

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
# MeshCore Protocol Specification
## Section 0: Overview
### Status and Provenance
> **This is an unofficial, independent specification.** It is **not** part of
> the upstream [MeshCore project](https://github.com/meshcore-dev/MeshCore),
> not affiliated with or endorsed by its maintainers, and not authored by
> them. Read this section before relying on the spec for anything
> load-bearing.
This document was primarily written by an AI (Claude, by Anthropic — mostly
the Opus 4.x family) under the direction of Stephen Waits, with human
proofreading and editing on top. Substantive edits — additions, audit passes,
errata fixes — are produced through model-driven analysis of the upstream
MeshCore C++ source and reviewed before commit.
The **upstream MeshCore C++ firmware is the source of truth** for all
protocol behavior. This spec is downstream and derivative. Whenever the spec
and the upstream C++ disagree, **upstream wins** — and a fix to this spec is
filed against this repo, not against MeshCore. Each substantive change in
[CHANGELOG.md](https://github.com/swaits/meshcore-spec/blob/main/CHANGELOG.md)
records the upstream commit hash it was validated against.
Refinements have also been driven in part by observations from building
DongLoRa firmware and apps against live MeshCore deployments. Where DongLoRa
behavior diverges from upstream, this spec follows upstream and treats the
DongLoRa side as a follow-up bug, not a spec change.
Conformance against the [test corpus](https://github.com/swaits/meshcore-spec/tree/main/corpus)
is a useful signal but **not a substitute** for cross-checking against
upstream firmware. If you find a divergence from upstream, please
[file an issue](https://github.com/swaits/meshcore-spec/issues) — that is
exactly the kind of report this project exists to catch.
### Spec Versions
The published site preserves prior spec versions alongside the rolling
`latest` build. The URL layout is:
- `https://swaits.github.io/meshcore-spec/latest/` — built from the tip of
`main`. May change between any two visits.
- `https://swaits.github.io/meshcore-spec/v0.1.0/` (and likewise for any
future `vX.Y.Z`) — a frozen content snapshot. Suitable for citing or for
certifying an implementation against.
Frozen versions live as their own directory in the repo
(`versions/vX.Y.Z/`) — a literal copy of `src/` taken at release time.
This deliberately separates **content** (versioned, frozen) from **site
infrastructure** (theme, picker, build pipeline; lives at the repo root
and is unversioned). Every deploy rebuilds every published version using
today's infrastructure, so site improvements show up everywhere without
backporting.
The version-picker in the top-right of every page lists every published
version; switching keeps you on the same chapter where it exists in the
target version. Spec versions use semver and are **independent** of the
upstream MeshCore firmware's own versioning — each release records the
upstream commit hash it was validated against in
[CHANGELOG.md](https://github.com/swaits/meshcore-spec/blob/main/CHANGELOG.md).
### Scope
This specification covers:
- The packet wire format (framing, header, path, payload)
- All payload types and their binary encodings
- Cryptographic operations (encryption, signing, key exchange, hashing)
- The BLE/Serial companion protocol
- The KISS modem protocol
- The bridge protocol (RS232/ESP-NOW)
It does NOT cover:
- Application-level behavior (e.g., room server logic, repeater policies)
- Radio-layer parameters (LoRa spreading factor, bandwidth, frequency)
- Duty cycle management or transmission scheduling
- User interface or companion app behavior
### Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
Additional terms used throughout:
| Term | Definition |
|------|-----------|
| **Packet** | The fundamental transmission unit in MeshCore. |
| **Header** | The first byte of a packet, encoding route type, payload type, and protocol version. |
| **Path** | A sequence of node hashes representing the route a packet has taken or should follow. |
| **Hash** | A truncated prefix of a node's Ed25519 public key, used for routing. |
| **Payload** | The data portion of a packet, whose structure depends on the payload type. |
| **Transport Codes** | Optional 4-byte field present in transport-mode packets, used for regional scoping. |
| **MAC** | Message Authentication Code — a 2-byte truncated HMAC-SHA256 used for integrity verification. |
| **MTU** | Maximum Transmission Unit — 255 bytes for MeshCore. |
| **Flood Routing** | Routing mode where packets are broadcast and repeated by intermediate nodes, building up the path as they travel. |
| **Direct Routing** | Routing mode where the path is supplied by the sender and the packet follows a specific route. |
### Byte Order
All multi-byte integer fields in MeshCore are encoded in **little-endian** byte
order unless explicitly stated otherwise. The one exception is CayenneLPP sensor
data, which uses big-endian encoding.
### Notation
- Binary data is shown as hex bytes separated by spaces: `0D 00 EF BE AD DE`
- Bit fields are shown in binary with MSB first: `0bVVPPPPRR`
- Bit numbering is LSB = bit 0, MSB = bit 7
- Field sizes are in bytes unless noted as bits
- `[field(N)]` denotes a field of N bytes
- `[field(N)?]` denotes an optional field of N bytes
### Protocol Constants
These constants define the fundamental limits of the protocol:
| Constant | Value | Description |
|----------|-------|-------------|
| `MAX_TRANS_UNIT` | 255 | Maximum packet size on the wire (bytes) |
| `MAX_PACKET_PAYLOAD` | 184 | Maximum payload size (bytes) |
| `MAX_PATH_SIZE` | 64 | Maximum path size (bytes) |
| `PUB_KEY_SIZE` | 32 | Ed25519 public key size (bytes) |
| `PRV_KEY_SIZE` | 64 | Ed25519 private key size (bytes) |
| `SIGNATURE_SIZE` | 64 | Ed25519 signature size (bytes) |
| `CIPHER_KEY_SIZE` | 16 | AES-128 key size (bytes) |
| `CIPHER_BLOCK_SIZE` | 16 | AES-128 block size (bytes) |
| `CIPHER_MAC_SIZE` | 2 | Truncated HMAC-SHA256 MAC size (bytes) |
| `PATH_HASH_SIZE` | 1 | Default path hash size for v1 (bytes) |
| `MAX_HASH_SIZE` | 8 | Maximum hash size for deduplication (bytes) |
| `MAX_ADVERT_DATA_SIZE` | 32 | Maximum advertisement app data size (bytes) |
### Protocol Versions
The protocol version is encoded in bits 6-7 of the header byte:
| Version | Value | Status | Description |
|---------|-------|--------|-------------|
| V1 | `0x00` | Active | 1-byte src/dest hashes, 2-byte MAC |
| V2 | `0x01` | Reserved | Future (e.g., 2-byte hashes, 4-byte MAC) |
| V3 | `0x02` | Reserved | Future |
| V4 | `0x03` | Reserved | Future |
Currently only V1 is defined. Implementations MUST support V1.
Implementations SHOULD reject packets with unrecognized version values unless
they have explicit support for that version.
### Document Organization
| Section | Title | Description |
|---------|-------|-------------|
| 00 | Overview | This document |
| 01 | Wire Format | Packet framing and field layout |
| 02 | Header | Header byte encoding |
| 03 | Path | Path length encoding and path field |
| 04 | Payload: ACK | Acknowledgment payload |
| 05 | Payload: Advertisement | Node advertisement and app data |
| 06 | Payload: Encrypted | REQ/RESPONSE/TXT_MSG payloads |
| 07 | Payload: Anonymous Request | Anonymous request payload |
| 08 | Payload: Group | Group text and data payloads |
| 09 | Payload: Path Return | Encrypted path return payload |
| 10 | Payload: Trace | Path trace payload |
| 11 | Payload: Multipart | Multi-packet payload |
| 12 | Payload: Control | Control and discovery payloads |
| 13 | Payload: Raw Custom | Custom raw payload |
| 14 | Cryptography | AES-128, HMAC-SHA256, encrypt-then-MAC |
| 15 | Identity | Ed25519 keys, ECDH, key hashing |
| 16 | Packet Hash | SHA-256 deduplication hashing |
| 17 | Routing | Flood and direct routing behavior |
| 18 | Companion Protocol | BLE/Serial companion communication |
| 19 | KISS Protocol | KISS modem framing and extensions |
| 20 | Bridge Protocol | RS232/ESP-NOW bridge framing |
### References
- [MeshCore Firmware](https://github.com/meshcore-dev/MeshCore) — Reference C++ implementation
- [MeshCore Documentation](https://docs.meshcore.io) — Official protocol documentation
- [MeshCore.js](https://github.com/meshcore-dev/meshcore.js) — JavaScript implementation
- [meshcore-rs](https://github.com/Duncaen/meshcore-rs) — Rust implementation (community)
- [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) — Key words for requirement levels
- [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) — Ed25519 digital signatures
- [FIPS 197](https://csrc.nist.gov/publications/detail/fips/197/final) — AES specification
- [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) — SHA-256 specification
+140
View File
@@ -0,0 +1,140 @@
# MeshCore Protocol Specification
## Section 1: Wire Format
### Overview
A MeshCore packet is the fundamental transmission unit. It consists of a 1-byte
header, an optional 4-byte transport codes field, a 1-byte path length, a
variable-length path, and a variable-length payload. The maximum packet size on
the wire is 255 bytes (MAX_TRANS_UNIT).
### Packet Layout
```
0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Header | Transport Codes (opt, 4B) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Path Length | Path (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The fields appear in this order on the wire:
```
[header(1)][transport_codes(4)?][path_len(1)][path(0-64)][payload(1-184)]
```
### Field Summary
| Field | Size | Required | Description |
|-------|------|----------|-------------|
| Header | 1 byte | Yes | Route type, payload type, protocol version (see [Section 2](02-header.md)) |
| Transport Codes | 4 bytes | Conditional | Two uint16_le values; present only when route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03) |
| Path Length | 1 byte | Yes | Encoded hash count and hash size (see [Section 3](03-path.md)) |
| Path | 0-64 bytes | Yes | Sequence of node hashes; length = hash_count × hash_size |
| Payload | 1-184 bytes | Yes | Packet data; structure depends on payload type |
### Transport Codes
Transport codes are present if and only if the route type (header bits 0-1) is
`TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03).
When present, transport codes consist of two little-endian 16-bit unsigned
integers, for a total of 4 bytes:
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code 1 (uint16_le) | Code 2 (uint16_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
- **Code 1**: Calculated from regional scope parameters
- **Code 2**: Reserved (typically 0)
When transport codes are not present (route types `FLOOD` and `DIRECT`), the
implementation MUST set both transport code values to zero internally.
### Encoding Algorithm
To encode a packet to wire format:
1. Write the header byte.
2. If the route type is `TRANSPORT_FLOOD` or `TRANSPORT_DIRECT`:
a. Write transport_codes[0] as uint16_le (2 bytes).
b. Write transport_codes[1] as uint16_le (2 bytes).
3. Write the path_len byte.
4. Calculate path byte length as `hash_count × hash_size` (see [Section 3](03-path.md)).
5. Write `path_byte_length` bytes from the path array.
6. Write `payload_len` bytes from the payload array.
The total wire length is:
```
1 + (hasTransportCodes ? 4 : 0) + 1 + path_byte_length + payload_len
```
This MUST NOT exceed MAX_TRANS_UNIT (255) bytes.
### Decoding Algorithm
To decode a packet from wire format:
1. Read 1 byte as the header. Extract route type from bits 0-1.
2. If route type is `TRANSPORT_FLOOD` (0x00) or `TRANSPORT_DIRECT` (0x03):
a. Read 2 bytes as transport_codes[0] (uint16_le).
b. Read 2 bytes as transport_codes[1] (uint16_le).
Otherwise, set both transport codes to zero.
3. Read 1 byte as path_len.
4. Validate path_len (see [Section 3](03-path.md)):
a. Extract hash_size = (path_len >> 6) + 1. If hash_size is 4, the packet
is INVALID.
b. Extract hash_count = path_len & 63.
c. If hash_count × hash_size > MAX_PATH_SIZE (64), the packet is INVALID.
5. Calculate path_byte_length = hash_count × hash_size.
6. Read path_byte_length bytes as the path.
7. Let `i` be the current read position. If `i >= total_length`, the packet is
INVALID (payload MUST contain at least 1 byte).
8. Calculate payload_len = total_length - i.
9. If payload_len > MAX_PACKET_PAYLOAD (184), the packet is INVALID.
10. Read payload_len bytes as the payload.
### Size Constraints
| Constraint | Value | Enforcement |
|-----------|-------|-------------|
| Maximum wire length | 255 bytes | Encoder MUST NOT produce packets exceeding this |
| Maximum payload | 184 bytes | Decoder MUST reject payloads exceeding this |
| Maximum path | 64 bytes | Decoder MUST reject paths exceeding this |
| Minimum payload | 1 byte | Decoder MUST reject packets with zero-length payload |
| Minimum packet | 3 bytes | Header (1) + path_len (1) + payload (1 minimum) |
### Error Conditions
A conforming decoder MUST reject packets with any of the following:
1. Total wire length less than 3 bytes (no room for header + path_len + payload)
2. Total wire length less than minimum required for the route type:
- 3 bytes for FLOOD or DIRECT
- 7 bytes for TRANSPORT_FLOOD or TRANSPORT_DIRECT (adds 4 transport code bytes)
3. Invalid path_len encoding (see [Section 3](03-path.md))
4. Path byte length exceeds remaining bytes
5. Zero-length payload after header, transport codes, path_len, and path
6. Payload length exceeding MAX_PACKET_PAYLOAD (184)
### Cross-References
- [Section 2: Header](02-header.md) — Header byte encoding details
- [Section 3: Path](03-path.md) — Path length encoding and path field
- Test vectors: `corpus/wire-format/framing/`, `corpus/wire-format/invalid/`
### Reference Implementation
- `Packet::writeTo()` in `src/Packet.cpp` — Encoding
- `Packet::readFrom()` in `src/Packet.cpp` — Decoding
- `Packet::getRawLength()` in `src/Packet.cpp` — Wire length calculation
+125
View File
@@ -0,0 +1,125 @@
# MeshCore Protocol Specification
## Section 2: Header
### Overview
The header is the first byte of every MeshCore packet. It encodes three fields
using bit packing: the route type (2 bits), the payload type (4 bits), and the
protocol version (2 bits).
### Bit Layout
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| V1 V0| P3 P2 P1 P0| R1 R0|
+---+---+---+---+---+---+---+---+
| | |
| | +-- Route Type (bits 0-1)
| +-------------- Payload Type (bits 2-5)
+------------------------ Protocol Version (bits 6-7)
```
The header byte value is computed as:
```
header = (version << 6) | (payload_type << 2) | route_type
```
And fields are extracted as:
```
route_type = header & 0x03
payload_type = (header >> 2) & 0x0F
version = (header >> 6) & 0x03
```
### Route Type (bits 0-1)
The route type determines how the packet is routed through the mesh and whether
transport codes are present.
| Value | Name | Transport Codes | Description |
|-------|------|----------------|-------------|
| 0x00 | TRANSPORT_FLOOD | Yes (4 bytes) | Flood routing with transport codes for regional scoping |
| 0x01 | FLOOD | No | Standard flood routing; path is built up by intermediate nodes |
| 0x02 | DIRECT | No | Direct routing; path is supplied by the sender |
| 0x03 | TRANSPORT_DIRECT | Yes (4 bytes) | Direct routing with transport codes |
Transport codes MUST be present in the wire format when route type is 0x00 or
0x03, and MUST NOT be present when route type is 0x01 or 0x02.
### Payload Type (bits 2-5)
The payload type identifies the structure of the payload data.
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | REQUEST | Encrypted request (dest_hash + src_hash + MAC + ciphertext) |
| 0x01 | RESPONSE | Encrypted response to REQ or ANON_REQ |
| 0x02 | TXT_MSG | Encrypted text message |
| 0x03 | ACK | Simple acknowledgment (4-byte CRC) |
| 0x04 | ADVERT | Node identity advertisement |
| 0x05 | GRP_TXT | Encrypted group text message |
| 0x06 | GRP_DATA | Encrypted group datagram |
| 0x07 | ANON_REQ | Anonymous request (full public key instead of hash) |
| 0x08 | PATH | Encrypted returned path information |
| 0x09 | TRACE | Path trace with per-hop SNR collection |
| 0x0A | MULTIPART | One part of a multi-packet sequence |
| 0x0B | CONTROL | Control/discovery packet |
| 0x0C | (reserved) | Reserved for future use |
| 0x0D | (reserved) | Reserved for future use |
| 0x0E | (reserved) | Reserved for future use |
| 0x0F | RAW_CUSTOM | Custom raw bytes for application-defined payloads |
Implementations SHOULD silently discard packets with reserved payload type values
(0x0C-0x0E) unless they have explicit support for extended types.
### Protocol Version (bits 6-7)
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | V1 | Current version: 1-byte src/dest hashes, 2-byte MAC |
| 0x01 | V2 | Reserved for future use |
| 0x02 | V3 | Reserved for future use |
| 0x03 | V4 | Reserved for future use |
Implementations MUST support version 0x00 (V1). Implementations SHOULD reject
packets with unrecognized version values.
### Special Header Values
The header value `0xFF` is used internally as a "do not retransmit" marker. This
is an in-memory sentinel only and MUST NOT appear on the wire. A header of
`0xFF` would decode as version=3, payload_type=0x0F (RAW_CUSTOM), route_type=3
(TRANSPORT_DIRECT).
### Encoding Examples
| Header Byte | Binary | Version | Payload Type | Route Type |
|-------------|--------|---------|-------------|------------|
| `0x01` | `00 000000 01` | V1 (0) | REQUEST (0) | FLOOD (1) |
| `0x05` | `00 000001 01` | V1 (0) | RESPONSE (1) | FLOOD (1) |
| `0x09` | `00 000010 01` | V1 (0) | TXT_MSG (2) | FLOOD (1) |
| `0x0D` | `00 000011 01` | V1 (0) | ACK (3) | FLOOD (1) |
| `0x11` | `00 000100 01` | V1 (0) | ADVERT (4) | FLOOD (1) |
| `0x0C` | `00 000011 00` | V1 (0) | ACK (3) | TRANSPORT_FLOOD (0) |
| `0x0E` | `00 000011 10` | V1 (0) | ACK (3) | DIRECT (2) |
| `0x0F` | `00 000011 11` | V1 (0) | ACK (3) | TRANSPORT_DIRECT (3) |
| `0x4D` | `01 000011 01` | V2 (1, reserved) | ACK (3) | FLOOD (1) |
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure
- [Section 3: Path](03-path.md) — Path encoding
- Test vectors: `corpus/wire-format/header/`
### Reference Implementation
- `Packet::getRouteType()` in `src/Packet.h` — `header & 0x03`
- `Packet::getPayloadType()` in `src/Packet.h` — `(header >> 2) & 0x0F`
- `Packet::getPayloadVer()` in `src/Packet.h` — `(header >> 6) & 0x03`
- `Packet::hasTransportCodes()` in `src/Packet.h` — route type 0x00 or 0x03
- Route type constants in `src/Packet.h`
- Payload type constants in `src/Packet.h`
+155
View File
@@ -0,0 +1,155 @@
# MeshCore Protocol Specification
## Section 3: Path
### Overview
The path field carries routing information as a sequence of node hashes. In
flood routing, intermediate nodes append their hash to the path as they forward
the packet. In direct routing, the sender supplies the full path. The path
length byte uses bit packing to encode both the number of hashes and the size of
each hash.
### Path Length Byte
The path length byte immediately follows the header (and transport codes, if
present). It encodes two values:
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| S1 S0| C5 C4 C3 C2 C1 C0|
+---+---+---+---+---+---+---+---+
| |
| +-- Hash Count (bits 0-5): 0-63
+----------------- Hash Size Code (bits 6-7): 0-2 (3 is reserved)
```
| Field | Bits | Extraction | Range | Description |
|-------|------|-----------|-------|-------------|
| Hash Count | 0-5 | `path_len & 0x3F` | 0-63 | Number of hashes in the path |
| Hash Size Code | 6-7 | `path_len >> 6` | 0-2 | Encoded hash size: actual size = code + 1 |
The **actual hash size** in bytes is:
```
hash_size = (path_len >> 6) + 1
```
| Hash Size Code | Actual Hash Size | Description |
|---------------|-----------------|-------------|
| 0 (`0b00`) | 1 byte | V1 default (PATH_HASH_SIZE) |
| 1 (`0b01`) | 2 bytes | Extended precision |
| 2 (`0b10`) | 3 bytes | Extended precision |
| 3 (`0b11`) | **RESERVED** | MUST be rejected as invalid |
The path length byte is constructed as:
```
path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F)
```
### Path Field
The path field immediately follows the path length byte. Its size in bytes is:
```
path_byte_length = hash_count × hash_size
```
The path contains `hash_count` consecutive hashes, each `hash_size` bytes long.
Each hash is a prefix of a node's Ed25519 public key (see [Section 15](15-identity.md)).
```
+----------+----------+-----+----------+
| Hash 0 | Hash 1 | ... | Hash N-1 |
| (H bytes)| (H bytes)| ... | (H bytes)|
+----------+----------+-----+----------+
where H = hash_size, N = hash_count
```
When hash_count is 0, the path field is empty (zero bytes on the wire).
### Validation Rules
A conforming implementation MUST reject a packet if any of the following are
true:
1. **Reserved hash size**: Hash size code is 3 (bits 6-7 = `0b11`), meaning
hash_size would be 4. This value is reserved for future use.
2. **Path overflow**: `hash_count × hash_size > MAX_PATH_SIZE` (64 bytes).
The maximum number of hashes depends on hash size:
| Hash Size | Max Hash Count | Max Path Bytes |
|-----------|---------------|----------------|
| 1 byte | 63 | 63 |
| 2 bytes | 32 | 64 |
| 3 bytes | 21 | 63 |
Note: With 1-byte hashes the maximum hash count is 63 (not 64), because the
count field is only 6 bits wide. With 2-byte hashes, 32 × 2 = 64 bytes exactly
fills MAX_PATH_SIZE. With 3-byte hashes, 21 × 3 = 63 bytes is the maximum
before 22 × 3 = 66 would overflow.
### Path Manipulation
**Appending a hash (flood routing):**
When a node forwards a flood-routed packet, it appends its own hash to the path:
1. Let `n` = current hash_count.
2. Verify `(n + 1) × hash_size <= MAX_PATH_SIZE`. If not, the node MUST NOT
forward the packet.
3. Copy the node's hash (first `hash_size` bytes of its public key) to
`path[n × hash_size]`.
4. Increment hash_count: `setPathHashCount(n + 1)`.
**Setting hash size and count:**
```
path_len = ((hash_size - 1) << 6) | (hash_count & 0x3F)
```
### Encoding Examples
| path_len byte | Binary | Hash Size Code | Hash Count | Hash Size | Path Bytes |
|---------------|--------|---------------|------------|-----------|------------|
| `0x00` | `00 000000` | 0 | 0 | 1 | 0 |
| `0x01` | `00 000001` | 0 | 1 | 1 | 1 |
| `0x03` | `00 000011` | 0 | 3 | 1 | 3 |
| `0x3F` | `00 111111` | 0 | 63 | 1 | 63 |
| `0x41` | `01 000001` | 1 | 1 | 2 | 2 |
| `0x42` | `01 000010` | 1 | 2 | 2 | 4 |
| `0x60` | `01 100000` | 1 | 32 | 2 | 64 |
| `0x81` | `10 000001` | 2 | 1 | 3 | 3 |
| `0x95` | `10 010101` | 2 | 21 | 3 | 63 |
| `0xC0` | `11 000000` | 3 | 0 | **INVALID** | — |
| `0xFF` | `11 111111` | 3 | 63 | **INVALID** | — |
### Known Implementation Discrepancy
The Rust implementation [meshcore-rs](https://github.com/Duncaen/meshcore-rs)
treats the path_len byte as a raw byte count rather than decoding hash_count and
hash_size from the bitfields. For example, a path_len value of `0x42` (binary
`01 000010`) should decode as hash_size=2, hash_count=2, path_bytes=4. The
incorrect interpretation reads it as a raw count of 66 bytes.
Test vectors in this corpus specifically exercise multi-byte hash sizes to catch
this class of bug.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Overall packet structure
- [Section 2: Header](02-header.md) — Header encoding
- [Section 15: Identity](15-identity.md) — Hash derivation from public keys
- Test vectors: `corpus/wire-format/path/`
### Reference Implementation
- `Packet::getPathHashSize()` in `src/Packet.h` — `(path_len >> 6) + 1`
- `Packet::getPathHashCount()` in `src/Packet.h` — `path_len & 63`
- `Packet::getPathByteLen()` in `src/Packet.h` — `getPathHashCount() * getPathHashSize()`
- `Packet::setPathHashSizeAndCount()` in `src/Packet.h` — `((sz - 1) << 6) | (n & 63)`
- `Packet::isValidPathLen()` in `src/Packet.cpp` — Validation logic
- `Packet::writePath()` in `src/Packet.cpp` — Path serialization
+99
View File
@@ -0,0 +1,99 @@
# MeshCore Protocol Specification
## Section 4: Payload — ACK
### Overview
The ACK (acknowledgment) payload is the simplest payload type. It consists of a
single 4-byte CRC value used to confirm receipt of a previously sent message.
The CRC is a truncated SHA-256 over the original TXT_MSG plaintext prefix
concatenated with the sender's public key.
### Payload Type
Header payload type field: `0x03` (ACK)
### Wire Format
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ACK CRC (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| ack_crc | 0 | 4 bytes | uint32_le | CRC identifying the acknowledged message |
### ACK CRC Computation
The 4-byte ACK CRC is computed as the first 4 bytes of a SHA-256 digest over
the acknowledged TXT_MSG's plaintext prefix followed by the **sender's**
Ed25519 public key:
```
ack_crc = SHA-256( plaintext_prefix || sender_pub_key )[0..3]
```
Where `plaintext_prefix` is the TXT_MSG plaintext up to and including the
message text, **excluding any trailing NUL terminator**:
```
plaintext_prefix = timestamp_le(4) || txt_type_attempt(1) || text
```
- `timestamp_le` and `txt_type_attempt` are defined in
[Section 6](06-payload-encrypted.md#plaintext-format-txt_msg).
- `text` is the raw UTF-8 message bytes with no NUL terminator.
- `sender_pub_key` is the full 32-byte Ed25519 public key of the node that
originated the TXT_MSG (i.e., the node expecting this ACK back).
For TXT_TYPE_SIGNED_PLAIN messages, the 4-byte signature prefix is included:
```
plaintext_prefix = timestamp_le(4) || txt_type_attempt(1) || signature(4) || text
ack_crc = SHA-256( plaintext_prefix || receiver_pub_key )[0..3]
```
Note the SIGNED_PLAIN variant hashes against the **receiver's** public key;
see `BaseChatMesh::onPeerDataRecv()`.
Because `txt_type_attempt` includes the 2-bit `attempt` counter, each retry
with an incremented `attempt` produces a different ACK CRC. This lets the
sender attribute each received ACK to a specific transmission attempt. See
[Section 6: Reliable DM Delivery](06-payload-encrypted.md#reliable-dm-delivery).
### Encoding
1. Compute the 4-byte CRC value for the message being acknowledged (above).
2. Write the CRC as a little-endian 32-bit unsigned integer.
3. Set payload_len to 4.
### Decoding
1. Read 4 bytes from the payload as a little-endian uint32.
2. If fewer than 4 bytes are available, the packet is INVALID.
### Constraints
- The payload MUST be exactly 4 bytes.
- Implementations SHOULD log or discard ACK packets with payload_len < 4.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet framing
- [Section 11: Multipart](11-payload-multipart.md) — Multipart ACK encoding
- Test vectors: `corpus/payloads/ack/`
### Reference Implementation
- `Mesh::createAck()` in `src/Mesh.cpp` — Packet construction
- `BaseChatMesh::composeMsgPacket()` in `src/helpers/BaseChatMesh.cpp` —
Sender-side expected-ACK computation (`expected_ack`)
- `BaseChatMesh::onPeerDataRecv()` in `src/helpers/BaseChatMesh.cpp` —
Receiver-side ACK CRC computation (`ack_hash`)
- ACK reception in `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ACK`
+123
View File
@@ -0,0 +1,123 @@
# MeshCore Protocol Specification
## Section 5: Payload — Advertisement
### Overview
The advertisement payload allows a node to announce its identity to the mesh. It
contains the node's Ed25519 public key, a timestamp, a signature proving
ownership of the key, and optional application data describing the node's type,
location, features, and name.
### Payload Type
Header payload type field: `0x04` (ADVERT)
### Wire Format
```
0 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ed25519 Public Key (32 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ed25519 Signature (64 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| App Data (0-32 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| pub_key | 0 | 32 bytes | raw | Ed25519 public key of the advertising node |
| timestamp | 32 | 4 bytes | uint32_le | Unix epoch timestamp of advertisement creation |
| signature | 36 | 64 bytes | raw | Ed25519 signature (see Signature section below) |
| app_data | 100 | 0-32 bytes | structured | Optional application data (see App Data section below) |
### Minimum and Maximum Sizes
- Minimum payload: 100 bytes (pub_key + timestamp + signature, no app_data)
- Maximum payload: 132 bytes (100 + MAX_ADVERT_DATA_SIZE of 32)
### Signature
The signature is computed over the concatenation of:
```
message = pub_key(32) || timestamp_le(4) || app_data(0-32)
```
The node signs this message using its Ed25519 private key. Receivers MUST verify
the signature using the public key from the payload. Packets with invalid
signatures MUST be discarded.
### App Data Format
When present (payload_len > 100), app_data is a structured field beginning with
a flags byte:
```
Bit: 7 6 5 4 3 2 1 0
+-------+-------+-------+-------+---+---+---+---+
|has_ |has_ |has_ |has_ | Node Type |
|name |feat2 |feat1 |loc | (4 bits) |
+-------+-------+-------+-------+---+---+---+---+
```
| Field | Offset | Size | Condition | Type | Description |
|-------|--------|------|-----------|------|-------------|
| flags | 0 | 1 byte | Always | uint8 | See bit layout above |
| latitude | 1 | 4 bytes | flags bit 4 set | int32_le | Latitude × 1,000,000 |
| longitude | 5 | 4 bytes | flags bit 4 set | int32_le | Longitude × 1,000,000 |
| feat1 | varies | 2 bytes | flags bit 5 set | uint16_le | Feature field 1 |
| feat2 | varies | 2 bytes | flags bit 6 set | uint16_le | Feature field 2 |
| name | varies | remaining | flags bit 7 set | UTF-8 | Node name (no null terminator) |
Fields appear in the order listed. The offset of each field depends on which
preceding optional fields are present.
### Node Types (flags bits 0-3)
| Value | Name | Description |
|-------|------|-------------|
| 0 | None | Unspecified |
| 1 | Chat | Chat client node |
| 2 | Repeater | Mesh repeater node |
| 3 | Room | Room server node |
| 4 | Sensor | Sensor node |
### Decoding Algorithm
1. Read 32 bytes as pub_key.
2. Read 4 bytes as timestamp (uint32_le).
3. Read 64 bytes as signature.
4. If offset (100) > payload_len, the packet is INVALID (incomplete).
5. If offset (100) == payload_len, there is no app_data. Done.
6. Read remaining bytes as app_data (up to MAX_ADVERT_DATA_SIZE = 32 bytes).
If more than 32 bytes remain, implementations MUST truncate to 32.
7. Construct the signature verification message: pub_key || timestamp_le || app_data.
8. Verify the Ed25519 signature. If invalid, the receiver MUST discard the packet.
9. Parse app_data:
a. Read 1 byte as flags.
b. If flags bit 4 set: read 4 bytes latitude (int32_le), 4 bytes longitude (int32_le).
c. If flags bit 5 set: read 2 bytes feat1 (uint16_le).
d. If flags bit 6 set: read 2 bytes feat2 (uint16_le).
e. If flags bit 7 set: read remaining bytes as UTF-8 name.
### Cross-References
- [Section 15: Identity](15-identity.md) — Ed25519 key management
- [Section 14: Cryptography](14-crypto.md) — Signature operations
- Test vectors: `corpus/payloads/advert/`
### Reference Implementation
- `Mesh::createAdvert()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ADVERT` in `src/Mesh.cpp` — Decoding and verification
+169
View File
@@ -0,0 +1,169 @@
# MeshCore Protocol Specification
## Section 6: Payload — Encrypted (REQ / RESPONSE / TXT_MSG)
### Overview
The REQUEST, RESPONSE, and TXT_MSG payload types share an identical wire format.
They consist of a destination hash, source hash, and an encrypt-then-MAC
ciphertext blob. The three types differ only in their semantic meaning at the
application layer.
### Payload Types
| Value | Name | Description |
|-------|------|-------------|
| 0x00 | REQUEST | Encrypted request (e.g., login, data query) |
| 0x01 | RESPONSE | Encrypted response to a REQUEST or ANON_REQ |
| 0x02 | TXT_MSG | Encrypted text message |
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| dest_hash | 0 | PATH_HASH_SIZE (1) | raw | First byte of recipient's Ed25519 public key |
| src_hash | 1 | PATH_HASH_SIZE (1) | raw | First byte of sender's Ed25519 public key |
| cipher_mac | 2 | CIPHER_MAC_SIZE (2) | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 4 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 4 + 16 = 20 bytes (dest_hash + src_hash + MAC + 1 AES block)
### Encryption
The ciphertext is produced using the encrypt-then-MAC scheme described in
[Section 14](14-crypto.md):
1. Compute the ECDH shared secret between sender and recipient.
2. Zero-pad the plaintext on the right with `0x00` bytes until its length is a
multiple of 16 (AES block size). If the plaintext is already a multiple of
16, no padding is added (the scheme is not unambiguous PKCS#7-style
padding — the receiver MUST disambiguate trailing zeros using the inner
plaintext format; see "Plaintext Format" below).
3. Encrypt the padded plaintext using AES-128-ECB with the first 16 bytes of
the shared secret as the key.
4. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared
secret as the HMAC key. Truncate to 2 bytes.
5. Prepend the 2-byte MAC to the ciphertext.
### Decryption
1. Extract dest_hash and src_hash.
2. If dest_hash matches this node, search for peers matching src_hash.
3. For each matching peer, compute or retrieve the shared secret.
4. Verify the MAC: compute HMAC-SHA256 over the ciphertext portion using the
full 32-byte shared secret. Compare the first 2 bytes with cipher_mac.
5. If the MAC is valid, decrypt the ciphertext using AES-128-ECB with the first
16 bytes of the shared secret. The decrypted data may contain trailing zero
bytes from padding.
6. If no peer's MAC matches, the packet is not for this node (or the peer is
unknown). The packet MAY still be forwarded.
### Plaintext Format (TXT_MSG)
For TXT_MSG payloads, the decrypted plaintext has this structure:
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| timestamp | 0 | 4 bytes | uint32_le | Message timestamp |
| txt_type_attempt | 4 | 1 byte | uint8 | Bits 2-7: message type (see [Section 4](04-payload-ack.md)); bits 0-1: attempt counter (0-3). See "Attempt Counter Semantics" below. |
| text | 5 | remaining | UTF-8 | Message text |
#### Attempt Counter Semantics
The `attempt` sub-field (bits 0-1 of `txt_type_attempt`) carries the low two
bits of a transmission attempt counter used by the sender's retry logic. For
a given logical TXT_MSG:
- The `timestamp` field MUST remain stable across all retries of the same
message.
- The `attempt` counter MUST be 0 on the first transmission and MUST be
incremented by 1 on each retry.
- Bits 0–1 of `txt_type_attempt` MUST always carry `attempt & 3`. These bits
are part of the ACK CRC input (see
[Section 4](04-payload-ack.md#ack-crc-computation)), so each retry of a
message produces a distinct expected ACK CRC.
- For attempts in `0..3`, no further encoding is needed; the plaintext ends
with the text.
- For attempts `> 3`, the sender MUST append a 2-byte tail
`[NUL_terminator(1)][attempt_full(1)]` to the plaintext after the text,
where `attempt_full` is the full 1-byte counter value (not just the low
2 bits). The receiver locates the text by scanning for the NUL terminator
and recovers the full attempt value from the trailing byte. Because this
tail costs 2 bytes of plaintext capacity, the maximum text length is
reduced by 2 bytes when `attempt > 3`.
- The ACK CRC is computed over `timestamp(4) || txt_type_attempt(1) || text`
only. The `[NUL][attempt_full]` tail (when present) and the implicit C
string NUL terminator (when not) are excluded from the hash input. This
means a retry with attempt 4 and a retry with attempt 0 produce ACK CRCs
that match (since `4 & 3 == 0`); senders relying on attempts beyond 3
SHOULD also verify the trailing attempt byte if they need to distinguish
rolled-over attempts.
See `BaseChatMesh::composeMsgPacket()` in
`src/helpers/BaseChatMesh.cpp` for the reference encoding.
### Reliable DM Delivery
The following sender-side behavior is informational and documents how
interoperating implementations deliver TXT_MSG reliably. Exact timeouts,
backoff schedules, and queue depths are implementation-defined; see
[Section 17 — Sender Behavior](17-routing.md#sender-behavior-informational)
for routing-layer details.
- **Expected-ACK tracking.** On transmit, the sender SHOULD compute the
expected `ack_crc` (see [Section 4](04-payload-ack.md#ack-crc-computation))
and record it in a pending-ACK table keyed by CRC. The table SHOULD bound
entries by capacity and age out entries after the retry schedule has
completed (plus a margin for in-flight ACKs).
- **Retry.** If no matching ACK is received within an implementation-defined
timeout, the sender SHOULD retransmit with `attempt` incremented (per
"Attempt Counter Semantics" above) and re-register the new expected
`ack_crc`. Up to 4 attempts are representable in the 2-bit `attempt`
field.
- **ACK consumption.** On receipt of a PAYLOAD_TYPE_ACK or a MULTIPART-wrapped
ACK whose `ack_crc` matches a pending entry, the sender MUST treat the
message as delivered, remove the pending-ACK entry, and cancel any queued
retries for that CRC.
- **Duplicate DM handling at the receiver.** A receiver MAY cache its most
recent reply per peer keyed by the inbound DM's plaintext and, on a
duplicate inbound DM within a short window, re-emit the cached reply as a
loss-recovery optimization. This is not normative; the normative behavior
is "process once and rely on [Section 16](16-packet-hash.md) dedup."
Implementations that re-emit cached replies SHOULD suppress re-emission
while a retry for that reply is still pending, to avoid piling on.
### Plaintext Format (REQUEST / RESPONSE)
The plaintext format for REQUEST and RESPONSE is application-defined. Common
patterns include:
- **REQUEST**: `[timestamp(4)][request_data...]`
- **RESPONSE**: `[timestamp(4)][response_data...]`
### Cross-References
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC details
- [Section 15: Identity](15-identity.md) — ECDH shared secret computation
- Test vectors: `corpus/payloads/encrypted/`
### Reference Implementation
- `Mesh::createDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_REQ/RESPONSE/TXT_MSG` — Decoding
- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encryption
- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — Decryption and verification
+74
View File
@@ -0,0 +1,74 @@
# MeshCore Protocol Specification
## Section 7: Payload — Anonymous Request
### Overview
The anonymous request payload allows a node to send an encrypted message to a
recipient without the recipient needing to know the sender in advance. Instead of
a 1-byte source hash, the full 32-byte Ed25519 public key of the sender is
included, enabling the recipient to compute the shared secret for decryption.
### Payload Type
Header payload type field: `0x07` (ANON_REQ)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | |
+-+-+-+-+-+-+-+-+ |
| Sender Public Key (32 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Cipher MAC (2 bytes) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| dest_hash | 0 | 1 byte | raw | First byte of recipient's Ed25519 public key |
| sender_pub_key | 1 | 32 bytes | raw | Full Ed25519 public key of sender |
| cipher_mac | 33 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 35 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 1 + 32 + 2 + 16 = 51 bytes
### Encryption
1. Sender computes ECDH shared secret using its private key and the recipient's
public key.
2. Encrypt plaintext using AES-128-ECB with first 16 bytes of shared secret.
3. Compute HMAC-SHA256 over ciphertext with full 32-byte shared secret.
Truncate to 2 bytes.
4. Assemble: dest_hash || sender_pub_key || MAC || ciphertext.
### Decryption
1. Read dest_hash. If it does not match this node, the packet is not for us
(but MAY be forwarded).
2. Read 32-byte sender_pub_key. Construct an Identity from it.
3. Compute ECDH shared secret between this node's private key and sender's
public key.
4. Verify the MAC and decrypt as in [Section 14](14-crypto.md).
### Cross-References
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Standard encrypted format
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- [Section 15: Identity](15-identity.md) — ECDH key exchange
- Test vectors: `corpus/payloads/anon-req/`
### Reference Implementation
- `Mesh::createAnonDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_ANON_REQ` — Decoding
+81
View File
@@ -0,0 +1,81 @@
# MeshCore Protocol Specification
## Section 8: Payload — Group (GRP_TXT / GRP_DATA)
### Overview
Group payloads enable encrypted communication within a channel. All members of a
channel share a symmetric secret. The payload begins with a 1-byte channel hash
(derived from the channel's shared key), followed by an encrypt-then-MAC blob.
### Payload Types
| Value | Name | Description |
|-------|------|-------------|
| 0x05 | GRP_TXT | Group text message |
| 0x06 | GRP_DATA | Group datagram (binary data) |
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|channel_hash(1)| Cipher MAC (2 bytes) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| channel_hash | 0 | 1 byte | raw | First byte of SHA-256(channel_shared_key) |
| cipher_mac | 1 | 2 bytes | raw | Truncated HMAC-SHA256 over ciphertext |
| ciphertext | 3 | variable (multiple of 16) | raw | AES-128-ECB encrypted data |
### Minimum Size
- Minimum payload: 1 + 2 + 16 = 19 bytes
### Channel Hash Derivation
The channel_hash is the first byte of the SHA-256 hash of the channel's 32-byte
shared key (PUB_KEY_SIZE bytes):
```
channel_hash = SHA256(channel_shared_key)[0]
```
This means hash collisions are expected (1 in 256 chance). When a receiver finds
a matching channel_hash, it attempts decryption with each matching channel's
secret. Only a successful MAC verification confirms the correct channel.
### Encryption
1. Encrypt plaintext using AES-128-ECB with first 16 bytes of the channel's
shared key (GroupChannel.secret).
2. Compute HMAC-SHA256 over ciphertext using all 32 bytes of the channel's
shared key. Truncate to 2 bytes.
3. Assemble: channel_hash || MAC || ciphertext.
### Decryption
1. Read channel_hash (1 byte).
2. Search local channel database for channels with matching hash. Multiple
channels may match (up to 4 in reference implementation).
3. For each matching channel, attempt MAC verification and decryption using the
channel's shared key.
4. The first channel that produces a valid MAC yields the correct decryption.
### Cross-References
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- Test vectors: `corpus/payloads/group/`
### Reference Implementation
- `Mesh::createGroupDatagram()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, cases `PAYLOAD_TYPE_GRP_TXT/GRP_DATA` — Decoding
- `Mesh::searchChannelsByHash()` — Channel lookup
+90
View File
@@ -0,0 +1,90 @@
# MeshCore Protocol Specification
## Section 9: Payload — Path Return
### Overview
The PATH payload carries an encrypted return path from one node to another. It
uses the same dest_hash + src_hash + encrypt-then-MAC envelope as REQ/RESPONSE/
TXT_MSG, but the decrypted contents contain routing path data and optional extra
application data.
### Payload Type
Header payload type field: `0x08` (PATH)
### Wire Format (Outer)
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| dest_hash(1) | src_hash(1) | Cipher MAC (2 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Ciphertext (N × 16 bytes, N ≥ 1) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The outer format is identical to [Section 6](06-payload-encrypted.md).
### Decrypted Inner Format
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| path_len(1) | Path (hash_count × hash_size) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| extra_type(1) | Extra Data (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
| Field | Size | Type | Description |
|-------|------|------|-------------|
| path_len | 1 byte | encoded | Same bitfield encoding as packet path_len (see [Section 3](03-path.md)) |
| path | hash_count × hash_size | raw | The return path to the sender |
| extra_type | 1 byte | uint8 | Type of extra data (lower 4 bits used; upper 4 reserved) |
| extra | remaining | raw | Extra data (may include trailing zero padding from AES) |
### Extra Type Values
| Value | Description |
|-------|-------------|
| 0x00 | No meaningful extra data (reciprocal path) |
| 0xFF | Dummy/padding (used when no real extra data; followed by 4 random bytes) |
| Other | Application-defined |
When creating a path return with no extra data, the reference implementation
appends `0xFF` as extra_type followed by 4 random bytes to ensure the packet
hash is unique.
### Encoding
1. Build the inner plaintext:
a. Write path_len byte (encoded hash_size and hash_count).
b. Write path bytes (hash_count × hash_size).
c. If extra data is provided: write extra_type (lower 4 bits), write extra.
d. If no extra data: write `0xFF`, write 4 random bytes.
2. Encrypt the inner plaintext using encrypt-then-MAC with the shared secret.
3. Prepend dest_hash and src_hash.
### Decoding
1. Decode the outer envelope (dest_hash, src_hash, MAC, ciphertext) as in
[Section 6](06-payload-encrypted.md).
2. After decryption:
a. Read 1 byte as path_len. Decode hash_size and hash_count.
b. Read hash_count × hash_size bytes as the path.
c. Read 1 byte as extra_type (use lower 4 bits only).
d. Remaining bytes are extra data (may include AES zero-padding).
### Cross-References
- [Section 3: Path](03-path.md) — Path length encoding
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Outer envelope format
- [Section 14: Cryptography](14-crypto.md) — Encrypt-then-MAC
- Test vectors: `corpus/payloads/path-return/`
### Reference Implementation
- `Mesh::createPathReturn()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, PATH case within REQ/RESPONSE/TXT_MSG handling — Decoding
+106
View File
@@ -0,0 +1,106 @@
# MeshCore Protocol Specification
## Section 10: Payload — Trace
### Overview
The TRACE payload enables path discovery with per-hop signal quality measurement.
A trace packet travels along a specified direct path, and each intermediate node
appends its received SNR value to the packet's path field. When the trace reaches
its final hop, the accumulated SNR values and node hashes are delivered.
### Payload Type
Header payload type field: `0x09` (TRACE)
### Wire Format
The trace payload has a fixed 9-byte header followed by an optional path hashes
section (appended when sent via direct routing):
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Tag (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Auth Code (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags | Path Hashes (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| tag | 0 | 4 bytes | uint32_le | Random tag set by the initiator for correlation |
| auth_code | 4 | 4 bytes | uint32_le | Authentication code |
| flags | 8 | 1 byte | uint8 | Flags (see below) |
| path_hashes | 9 | variable | raw | Node hashes for the path to trace (appended for direct send) |
### Flags Field
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Reserved | S1 S0|
+---+---+---+---+---+---+---+---+
```
| Bits | Description |
|------|-------------|
| 0-1 | Path hash size for the trace path hashes (as a power of 2: 0=1 byte, 1=2 bytes, 2=4 bytes) |
| 2-7 | Reserved (must be zero) |
Note: The flags field path hash size encoding (`1 << (flags & 0x03)`) differs
from the packet path_len encoding (`(code >> 6) + 1`).
### Direct Sending
When a TRACE packet is sent via direct routing, the path hashes are appended
directly to the payload (after the 9-byte header), not placed in the packet's
path field. The packet's path field is instead used to accumulate SNR values
from intermediate nodes.
```
On creation: payload = [tag(4)][auth_code(4)][flags(1)] (9 bytes)
On sendDirect: payload += path_hashes (9 + N bytes)
packet.path_len = 0 (path used for SNR)
```
### SNR Accumulation
As each intermediate node forwards a TRACE packet, it appends its received SNR
value as a single signed byte to the packet's path field:
```
path[path_len++] = (int8_t)(SNR × 4)
```
The SNR value is stored as SNR × 4 for 0.25 dB precision, matching the format
used in RxMeta frames.
### Trace Completion
A TRACE is considered complete when the path_len counter (used as an offset into
the path hashes in the payload) reaches or exceeds the remaining path hashes.
At that point, `onTraceRecv` is called with:
- The accumulated SNR values (in packet.path)
- The path hashes (in packet.payload, after byte 9)
- The path length (number of hops traversed)
### Minimum Size
- Minimum payload: 9 bytes (tag + auth_code + flags, no path hashes)
### Cross-References
- [Section 16: Packet Hash](16-packet-hash.md) — TRACE packets include path_len in hash
- Test vectors: `corpus/payloads/trace/`
### Reference Implementation
- `Mesh::createTrace()` in `src/Mesh.cpp` — Creation (9-byte base)
- `Mesh::sendDirect()` — Path appended to payload for TRACE type
- `Mesh::onRecvPacket()`, TRACE handling — SNR accumulation and completion check
+124
View File
@@ -0,0 +1,124 @@
# MeshCore Protocol Specification
## Section 11: Payload — Multipart
### Overview
The MULTIPART payload wraps another payload type to indicate it is part of a
multi-packet sequence. The first byte encodes the number of remaining packets in
the sequence and the sub-payload type. Currently, only multipart ACK is defined
in the reference implementation.
### Payload Type
Header payload type field: `0x0A` (MULTIPART)
### Wire Format
```
0 1
0 1 2 3 4 5 6 7 8 9 0 ...
+-+-+-+-+-+-+-+-+-+-+-+-+
|Rem| SubType | Sub-payload |
+-+-+-+-+-+-+-+-+-+-+-+-+
```
First byte encoding:
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Remaining (4) | Sub-Type (4) |
+---+---+---+---+---+---+---+---+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| remaining | 0 (bits 4-7) | 4 bits | uint | Number of remaining packets in sequence (0-15) |
| sub_type | 0 (bits 0-3) | 4 bits | uint | Payload type of the wrapped content |
| sub_payload | 1 | variable | raw | The wrapped payload data |
```
first_byte = (remaining << 4) | (sub_type & 0x0F)
```
### Multipart ACK
The only currently defined multipart sub-type is ACK (sub_type = 0x03):
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (rem<<4)|0x03| ACK CRC (uint32_le) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
Total payload: 5 bytes (1 byte header + 4 byte CRC).
Multipart ACKs are used to send additional ACK retransmissions for reliability.
The `remaining` field indicates how many more ACK packets follow this one,
counting down: a chain of K extra copies is emitted as MULTIPART with
`remaining = K, K-1, …, 1`, followed by exactly one final plain
`PAYLOAD_TYPE_ACK` packet (NOT a MULTIPART with `remaining = 0`). All copies
in the chain carry the same `ack_crc`. Receivers MUST treat the first copy
received as the authoritative ACK and dedup subsequent copies by `ack_crc`
at the application layer.
### Multipart ACK Usage Constraints
- A sender MUST only emit MULTIPART-wrapped ACKs on direct-routed return
paths (i.e., when a direct path to the original sender is known). When the
return path is unknown, the sender MUST fall back to emitting a plain
PAYLOAD_TYPE_ACK via flood routing. This reflects the firmware behavior in
`Mesh::routeDirectRecvAcks()` (which emits MULTIPART copies only when a
direct path is available) and `BaseChatMesh::sendAckTo()` (which falls back
to `sendFloodScoped()` when `out_path_len == OUT_PATH_UNKNOWN`).
- All MULTIPART-ACK copies and the final plain ACK for the same acknowledged
message carry an identical 4-byte `ack_crc`. Receivers MUST treat them as
the same logical ACK: the first one received satisfies the acknowledgment,
and subsequent duplicates MUST be deduplicated per
[Section 16](16-packet-hash.md) (each copy has a distinct packet hash
because the wrapper byte differs, so ordinary dedup does not suppress them
on the wire — application-level dedup by `ack_crc` is required).
- The typical inter-copy delay in the reference implementation is
approximately 300 ms plus the direct retransmit delay
(`Mesh::routeDirectRecvAcks()`); exact timing is implementation-defined.
- The number of extra copies is configured via `getExtraAckTransmitCount()`
and is implementation-defined. The firmware default is 0 (plain ACK only).
A receiver MUST NOT infer delivery reliability from the `remaining` counter
alone: the counter is a hint about how many additional copies the sender
intends to emit, not a guarantee that they will arrive.
### Encoding (Multipart ACK)
1. Set first byte to `(remaining << 4) | PAYLOAD_TYPE_ACK`.
2. Copy the 4-byte ACK CRC starting at offset 1.
3. Set payload_len to 5.
### Decoding (Multipart ACK)
1. Read first byte. Extract remaining = byte >> 4, sub_type = byte & 0x0F.
2. If sub_type == 0x03 (ACK) and payload_len >= 5:
a. Read 4 bytes at offset 1 as ACK CRC (uint32_le).
3. For other sub_types: reserved for future use.
### Constraints
- Payload MUST be at least 2 bytes (1 header + 1 sub-payload minimum).
- For multipart ACK, payload MUST be at least 5 bytes.
### Cross-References
- [Section 4: ACK](04-payload-ack.md) — ACK CRC format
- Test vectors: `corpus/payloads/multipart/`
### Reference Implementation
- `Mesh::createMultiAck()` in `src/Mesh.cpp` — Encoding
- `Mesh::routeDirectRecvAcks()` in `src/Mesh.cpp` — Direct-only gating and
inter-copy spacing
- `BaseChatMesh::sendAckTo()` in `src/helpers/BaseChatMesh.cpp` — Flood
fallback when no direct return path is known
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_MULTIPART` — Decoding
+68
View File
@@ -0,0 +1,68 @@
# MeshCore Protocol Specification
## Section 12: Payload — Control
### Overview
The CONTROL payload carries control and discovery data. Control packets are raw
byte payloads whose structure is determined by the first byte. A subset of
control packets (those with bit 7 of the first byte set) are restricted to
zero-hop delivery only.
### Payload Type
Header payload type field: `0x0B` (CONTROL)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Control Byte | Data... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| control_byte | 0 | 1 byte | uint8 | Control type identifier |
| data | 1 | variable | raw | Control-specific data |
### Zero-Hop Restriction
When bit 7 (`0x80`) of the control byte is set, the packet is restricted to
zero-hop delivery only. The reference implementation enforces this:
- If `(payload[0] & 0x80) != 0` AND the packet is direct-routed:
- It is processed only if `getPathHashCount() == 0`.
- It is NOT forwarded to other nodes.
### Discovery Protocols
Control packets are commonly used for node discovery. The specific sub-protocol
formats are application-defined, but typically include:
- **Discovery Request**: Sent zero-hop to find nearby nodes
- **Discovery Response**: Contains node type, name, SNR, and other metadata
### Encoding
1. Construct the raw control data bytes.
2. If the control packet should be zero-hop only, set bit 7 of the first byte.
3. Copy data to payload, set payload_len.
### Constraints
- Payload MUST be at least 1 byte (the control byte).
- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD).
### Cross-References
- [Section 17: Routing](17-routing.md) — Zero-hop delivery
- Test vectors: `corpus/payloads/control/`
### Reference Implementation
- `Mesh::createControlData()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, CONTROL handling — Zero-hop check and delivery
+49
View File
@@ -0,0 +1,49 @@
# MeshCore Protocol Specification
## Section 13: Payload — Raw Custom
### Overview
The RAW_CUSTOM payload carries application-defined raw bytes with no prescribed
structure. It is intended for applications that implement their own encryption,
framing, or payload formats on top of the MeshCore transport.
### Payload Type
Header payload type field: `0x0F` (RAW_CUSTOM)
### Wire Format
```
0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Raw Data (1-184 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
### Fields
| Field | Offset | Size | Type | Description |
|-------|--------|------|------|-------------|
| data | 0 | 1-184 bytes | raw | Application-defined payload |
### Routing
In the current reference implementation, RAW_CUSTOM packets are only processed
when received via direct routing. They are NOT flood-routed.
### Constraints
- Payload MUST be at least 1 byte.
- Maximum payload: 184 bytes (MAX_PACKET_PAYLOAD).
- Maximum total data: limited by `sizeof(Packet::payload)` which is
MAX_PACKET_PAYLOAD (184).
### Cross-References
- Test vectors: `corpus/payloads/raw-custom/`
### Reference Implementation
- `Mesh::createRawData()` in `src/Mesh.cpp` — Encoding
- `Mesh::onRecvPacket()`, case `PAYLOAD_TYPE_RAW_CUSTOM` — Processing (direct only)
+120
View File
@@ -0,0 +1,120 @@
# MeshCore Protocol Specification
## Section 14: Cryptography
### Overview
MeshCore uses a combination of symmetric and asymmetric cryptography. Data
confidentiality is provided by AES-128 in ECB mode with zero padding. Data
integrity is provided by HMAC-SHA256 truncated to 2 bytes. These are combined
in an encrypt-then-MAC scheme.
### Algorithms
| Operation | Algorithm | Key Size | Output |
|-----------|-----------|----------|--------|
| Symmetric Encryption | AES-128-ECB | 16 bytes | Multiple of 16 bytes |
| Message Authentication | HMAC-SHA256 | 32 bytes | 2 bytes (truncated) |
| Digital Signature | Ed25519 | 32/64 bytes | 64 bytes |
| Key Exchange | X25519 (ECDH) | 32 bytes | 32 bytes |
| Hashing | SHA-256 | — | Up to 32 bytes |
### AES-128-ECB Encryption
MeshCore uses AES-128 in Electronic Codebook (ECB) mode with zero-byte padding.
**Key**: The first 16 bytes (CIPHER_KEY_SIZE) of the shared secret.
**Encryption**:
1. Set the AES-128 key to `shared_secret[0..15]`.
2. Process the plaintext in 16-byte blocks:
a. For each complete 16-byte block, encrypt in place.
b. For the final partial block (< 16 bytes):
- Copy the remaining bytes to a 16-byte buffer.
- Fill remaining bytes with zeros.
- Encrypt the padded block.
3. If the plaintext is empty, encrypt a 16-byte all-zero block.
4. The output is always a multiple of 16 bytes.
**Decryption**:
1. Set the AES-128 key to `shared_secret[0..15]`.
2. Process the ciphertext in 16-byte blocks, decrypting each.
3. The output length equals the ciphertext length.
4. The final block MAY contain trailing zero bytes from padding.
The application must know the original plaintext length or use
a length-prefixed format to determine where meaningful data ends.
### HMAC-SHA256 (Truncated to 2 Bytes)
MeshCore computes HMAC-SHA256 over the ciphertext and truncates the result to
CIPHER_MAC_SIZE (2) bytes.
**Key**: The full 32 bytes (PUB_KEY_SIZE) of the shared secret.
Note: The HMAC key uses the full 32-byte shared secret, while the AES key uses
only the first 16 bytes. This is an important distinction.
**Computation**:
```
mac = HMAC-SHA256(key=shared_secret[0..31], message=ciphertext)[0..1]
```
The first 2 bytes of the HMAC-SHA256 output are used as the MAC.
### Encrypt-then-MAC
The encrypt-then-MAC scheme combines AES-128-ECB encryption with HMAC-SHA256
authentication:
**Encoding** (`encryptThenMAC`):
1. Encrypt the plaintext using AES-128-ECB (see above).
Store ciphertext at `dest[2..]` (offset by MAC size).
2. Compute HMAC-SHA256 over the ciphertext using the full 32-byte shared
secret. Truncate to 2 bytes.
3. Store the 2-byte MAC at `dest[0..1]`.
4. Return total length: 2 + ciphertext_length.
Output format:
```
[MAC (2 bytes)][Ciphertext (N × 16 bytes)]
```
**Decoding** (`MACThenDecrypt`):
1. If input length ≤ 2 (CIPHER_MAC_SIZE), return failure (invalid).
2. Recompute HMAC-SHA256 over `src[2..]` (the ciphertext portion) using the
full 32-byte shared secret. Truncate to 2 bytes.
3. Compare the computed MAC with `src[0..1]`.
4. If MACs do not match, return failure (0 = invalid HMAC).
5. If MACs match, decrypt `src[2..]` using AES-128-ECB.
6. Return the decrypted plaintext length.
### Security Notes
- **ECB mode**: AES-ECB encrypts each 16-byte block independently. Identical
plaintext blocks produce identical ciphertext blocks. This is a known
weakness of ECB mode, but MeshCore's short payloads and mesh-network context
make this acceptable for its use case.
- **2-byte MAC**: The truncated HMAC provides only 16 bits of authentication.
This means there is approximately a 1-in-65536 chance of a forged MAC being
accepted. This trade-off prioritizes bandwidth over authentication strength.
- **Key derivation**: The shared secret is used directly as both AES key
(first 16 bytes) and HMAC key (full 32 bytes). No key derivation function
(KDF) is applied.
### Cross-References
- [Section 6: Encrypted Payloads](06-payload-encrypted.md) — Usage in packet payloads
- [Section 15: Identity](15-identity.md) — Shared secret computation via ECDH
- Test vectors: `corpus/crypto/`
### Reference Implementation
- `Utils::encrypt()` in `src/Utils.cpp` — AES-128-ECB encryption
- `Utils::decrypt()` in `src/Utils.cpp` — AES-128-ECB decryption
- `Utils::encryptThenMAC()` in `src/Utils.cpp` — Encrypt-then-MAC
- `Utils::MACThenDecrypt()` in `src/Utils.cpp` — MAC-then-Decrypt
+92
View File
@@ -0,0 +1,92 @@
# MeshCore Protocol Specification
## Section 15: Identity
### Overview
Every MeshCore node has an Ed25519 key pair. The 32-byte public key serves as
the node's identity. Hashes used in routing are truncated prefixes of the public
key. Shared secrets for encryption are computed via ECDH key exchange using
X25519 (Ed25519 keys transposed to Curve25519).
### Key Types
| Type | Size | Description |
|------|------|-------------|
| Public Key | 32 bytes (PUB_KEY_SIZE) | Ed25519 public key |
| Private Key | 64 bytes (PRV_KEY_SIZE) | Ed25519 private key (seed + public) |
| Signature | 64 bytes (SIGNATURE_SIZE) | Ed25519 signature |
| Shared Secret | 32 bytes (PUB_KEY_SIZE) | ECDH shared secret |
### Hash Derivation
A node's routing hash is simply a prefix of its public key:
```
hash = pub_key[0..hash_size-1]
```
Where `hash_size` is 1, 2, or 3 bytes depending on the protocol version and
path encoding (see [Section 3](03-path.md)).
For V1, `PATH_HASH_SIZE` is 1, meaning a node's hash is the first byte of its
public key. This gives only 256 unique hash values, so hash collisions are
common and expected. The protocol handles this by attempting decryption with all
peers matching a given hash.
### Hash Matching
To check if a hash matches a node's identity:
```
match = memcmp(hash, pub_key, hash_size) == 0
```
### Ed25519 Signing
Used for advertisement signatures (see [Section 5](05-payload-advert.md)):
1. Construct the message to sign (e.g., pub_key || timestamp || app_data).
2. Sign using the Ed25519 private key.
3. The 64-byte signature is included in the advertisement payload.
Verification:
1. Extract the public key and signature from the payload.
2. Reconstruct the message.
3. Verify the signature using Ed25519 verification.
4. Receivers MUST discard the packet if verification fails.
### ECDH Key Exchange (X25519)
To compute a shared secret between two nodes:
1. Convert the Ed25519 keys to X25519 (Curve25519) format.
2. Perform X25519 Diffie-Hellman: `shared_secret = X25519(my_private, their_public)`.
3. The resulting 32-byte shared secret is used for:
- AES-128 key: `shared_secret[0..15]`
- HMAC-SHA256 key: `shared_secret[0..31]`
### Identity Serialization
Identities can be serialized for storage:
- **Public Identity**: 32 bytes (public key only)
- **Local Identity**: 64 bytes (private key) + 32 bytes (public key) = 96 bytes
Hex representation uses uppercase characters with 2 hex digits per byte.
### Cross-References
- [Section 3: Path](03-path.md) — Hash size in path encoding
- [Section 5: Advertisement](05-payload-advert.md) — Signature usage
- [Section 14: Cryptography](14-crypto.md) — Shared secret usage
- Test vectors: `corpus/crypto/ed25519/`, `corpus/crypto/ecdh/`
### Reference Implementation
- `Identity` class in `src/Identity.h` — Public key and verification
- `LocalIdentity` class in `src/Identity.h` — Key pair, signing, ECDH
- `Identity::copyHashTo()` — Hash derivation (prefix copy)
- `Identity::isHashMatch()` — Hash comparison
- `LocalIdentity::calcSharedSecret()` — ECDH key exchange
+67
View File
@@ -0,0 +1,67 @@
# MeshCore Protocol Specification
## Section 16: Packet Hash
### Overview
MeshCore uses SHA-256 hashing for packet deduplication. Each node maintains a
table of recently seen packet hashes and discards packets it has already
processed. The hash is computed over the payload type and payload contents, with
a special case for TRACE packets.
### Hash Computation
The packet hash is computed as:
```
hash = SHA-256(payload_type_byte || payload_data)[0..MAX_HASH_SIZE-1]
```
Where:
- `payload_type_byte` is a single byte: `(header >> 2) & 0x0F`
- `payload_data` is the raw payload bytes (payload_len bytes)
- The SHA-256 output is truncated to MAX_HASH_SIZE (8) bytes
### TRACE Packet Special Case
For TRACE packets (payload_type = 0x09), the path_len byte is included in the
hash to distinguish trace packets that revisit the same node on their return
path:
```
hash = SHA-256(payload_type_byte || path_len_byte || payload_data)[0..MAX_HASH_SIZE-1]
```
### Algorithm
```
function calculatePacketHash(packet):
sha = SHA256_init()
type_byte = getPayloadType(packet.header) // single byte
SHA256_update(sha, type_byte)
if type_byte == 0x09: // TRACE
SHA256_update(sha, packet.path_len) // 1 byte (as uint8)
SHA256_update(sha, packet.payload, packet.payload_len)
hash = SHA256_finalize(sha)
return hash[0..7] // first MAX_HASH_SIZE bytes
```
### Properties
- The hash does NOT include the header byte, path, or transport codes. This
means the same logical message received via different routes produces the
same hash.
- The hash does NOT include the route type or version. Only the payload type
and payload content determine the hash.
- For TRACE packets, including path_len ensures that the same trace payload
at different stages of traversal produces different hashes.
### Cross-References
- [Section 10: Trace](10-payload-trace.md) — TRACE packet special handling
- Test vectors: `corpus/crypto/sha256/packet-hash.json`
### Reference Implementation
- `Packet::calculatePacketHash()` in `src/Packet.cpp`
- `MeshTables::hasSeen()` — Deduplication table lookup
+208
View File
@@ -0,0 +1,208 @@
# MeshCore Protocol Specification
## Section 17: Routing
### Overview
MeshCore supports two routing modes: flood routing and direct routing. Flood
routing broadcasts packets to all neighbors, building up a path as the packet
propagates. Direct routing sends packets along a pre-determined path.
### Flood Routing
In flood routing, the sender emits a packet with an empty path. Each
intermediate node that forwards the packet appends its hash to the path before
retransmitting.
**Sending (flood)**:
1. Set route type to FLOOD (0x01) or TRANSPORT_FLOOD (0x00).
2. Set path to empty: `setPathHashSizeAndCount(hash_size, 0)`.
3. Transmit the packet.
**Forwarding (flood)**:
1. Receive a flood-routed packet.
2. Check the deduplication table. If already seen, discard.
3. Process the packet (decrypt if addressed to this node, etc.).
4. If the packet should be forwarded (node is a repeater):
a. Verify `(hash_count + 1) × hash_size <= MAX_PATH_SIZE`.
b. Append this node's hash at `path[hash_count × hash_size]`.
c. Increment hash_count.
d. Retransmit with a random delay.
**Path growth**: The path grows by one hash per hop. When the path reaches
MAX_PATH_SIZE (64 bytes), no more nodes can be appended and the packet stops
propagating.
### Direct Routing
In direct routing, the sender provides the complete path. Each intermediate
node checks whether it is the next hop, removes itself from the path, and
forwards to the next node.
**Sending (direct)**:
1. Set route type to DIRECT (0x02) or TRANSPORT_DIRECT (0x03).
2. Set the path to the destination's known route.
3. Transmit the packet.
**Forwarding (direct)**:
1. Receive a direct-routed packet.
2. Check if the first hash in the path matches this node.
3. If yes, and the node allows forwarding:
a. Remove the first hash from the path (shift remaining hashes left).
b. Decrement hash_count.
c. Retransmit.
4. If no, discard (this node is not the next hop).
**Path removal** (`removeSelfFromPath`):
1. Decrement hash_count.
2. Shift the path array: for each index k from 0 to (hash_count × hash_size),
copy `path[k + hash_size]` to `path[k]`.
### Zero-Hop Delivery
Zero-hop packets are direct-routed packets with hash_count = 0. They reach only
immediate neighbors (nodes within radio range). Used for:
- Control packets with bit 7 set in the control byte
- Discovery protocols
- Local-only operations
### Transport Codes
Transport codes (present in TRANSPORT_FLOOD and TRANSPORT_DIRECT) enable
regional scoping. Nodes can filter packets based on transport codes, accepting
only packets from their region.
### TRACE Routing
TRACE packets are a special case. They are sent via direct routing, but instead
of the normal path field, the path hashes are appended to the payload. The
packet's path field is repurposed to accumulate SNR values from each hop
(see [Section 10](10-payload-trace.md)).
### Retransmission Priority
Packets are prioritized for transmission:
| Priority | Packet Type | Description |
|----------|-------------|-------------|
| 0 (highest) | Direct routed | Routed traffic |
| 1 | Path return, standard | Most flood packets |
| 2 | Path return (flood) | Path packets |
| 3 | Advertisement | De-prioritized |
| 5 | Trace | Trace forwarding |
| N (hash_count) | Flood forwarded | Lower priority for more distant sources |
### Sender Behavior (Informational)
This section documents sender-side behavior that is not fully constrained by
the wire protocol but is common across interoperating implementations. The
reference firmware establishes the baseline; client implementations SHOULD
follow these patterns to interoperate cleanly. Specific numeric tuning
parameters (timeouts, queue sizes, retry counts) are implementation-defined.
#### Path Learning
- When a node receives a flood-routed packet from a peer, the accumulated
`path` field records the hops the packet traversed. The receiving node
SHOULD cache the **reversed** path keyed by the peer's source hash and
subsequently use that cached path to send direct-routed traffic back.
- When multiple paths are observed for the same peer, the **reference
implementation unconditionally replaces** the cached path with the most
recently observed one (`BaseChatMesh::onContactPathRecv()` in
`src/helpers/BaseChatMesh.cpp` — its source comment explicitly flags
hash-size or SNR-based selection as future work). Implementations MAY
layer a quality heuristic on top (e.g., prefer larger hash sizes or
better SNR margin) if they store and compare multiple candidate paths,
but this is not part of the reference behavior and the exact policy is
implementation-defined.
- Cached paths in the reference implementation do **not** expire on a
timer; they live as long as the contact entry, and are overwritten
whenever a fresher path is observed. Implementations MAY add a TTL or
age-based eviction policy as an extension. The reference implementation
does not persist paths across reboots.
#### PATH_RETURN
After receiving a flood-routed DM, a receiver MAY proactively emit a
PAYLOAD_TYPE_PATH packet ([Section 9](09-payload-path.md)) that embeds the
reversed path and optionally an ACK as extra data. This lets the original
sender learn a direct return path before its first reply, converting
subsequent traffic from flood to direct routing. See
`BaseChatMesh::onPeerDataRecv()` and `Mesh::createPathReturn()`.
The reference implementation schedules the PATH_RETURN transmission with a
small delay (`TXT_ACK_DELAY`, default 200 ms) to avoid colliding with other
packets the receiver is about to emit. Exact timing is
implementation-defined; the goal is to space PATH_RETURN, ACK, and any
application reply across distinct on-air slots.
#### Flood Fallback and Retries
- A sender SHOULD track outstanding DMs by their expected `ack_crc`
(see [Section 4](04-payload-ack.md)) and retry transmissions that are not
acknowledged within a timeout. Timeout values and backoff schedules are
implementation-defined and typically depend on LoRa airtime parameters.
- On each retry, the sender MUST increment the `attempt` sub-field of
`txt_type_attempt` (see
[Section 6](06-payload-encrypted.md#plaintext-format-txt_msg)). The
`timestamp` field MUST be held constant across retries of the same logical
message. This changes the ACK CRC per attempt, letting the sender attribute
a returned ACK to a specific transmission.
- After N unsuccessful direct-routed retries, the sender SHOULD discard the
cached direct path and retry via flood routing, so that a stale cached path
does not indefinitely block delivery. The value of N is
implementation-defined (the reference is small — single-digit).
- On receipt of a matching ACK (plain or MULTIPART, per
[Section 11](11-payload-multipart.md#multipart-ack-usage-constraints)),
the sender MUST cancel any queued retries for that ACK CRC.
#### Transmission Priority (sender-originated)
Client implementations that maintain a TX queue SHOULD prioritize
sender-originated packets roughly as follows (highest first):
| Priority | Packet Type |
|----------|-------------|
| 0 | ACK (plain or MULTIPART) |
| 1 | PATH / PATH_RETURN |
| 2 | Direct-routed reply (DM) |
| 3 | Flood-routed reply (DM) |
| 4 | Request/response (e.g., login, keep-alive) |
| 5 | Group text / group data |
| 6 | Advertisement |
This ordering complements the forwarding-priority table above: ACKs and
PATH returns are time-sensitive and short; direct replies take precedence
over flood replies to reduce channel occupancy. Exact ordering and queue
depth are implementation-defined.
### Packet Deduplication
Before processing or forwarding any packet, nodes check if the packet has
already been seen using `hasSeen()` with the packet hash from
[Section 16](16-packet-hash.md). This prevents infinite loops in flood routing.
Once a packet is marked as seen, it is NOT forwarded again even if received via
a different route. This is a "first packet wins" approach.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet structure
- [Section 3: Path](03-path.md) — Path encoding
- [Section 10: Trace](10-payload-trace.md) — TRACE routing special case
- [Section 16: Packet Hash](16-packet-hash.md) — Deduplication
### Reference Implementation
- `Mesh::sendFlood()` in `src/Mesh.cpp` — Flood send
- `Mesh::sendDirect()` in `src/Mesh.cpp` — Direct send
- `Mesh::sendZeroHop()` in `src/Mesh.cpp` — Zero-hop send
- `Mesh::routeRecvPacket()` in `src/Mesh.cpp` — Flood forwarding
- `Mesh::removeSelfFromPath()` in `src/Mesh.cpp` — Path manipulation
- `Mesh::onRecvPacket()` in `src/Mesh.cpp` — Direct forwarding
+96
View File
@@ -0,0 +1,96 @@
# MeshCore Protocol Specification
## Section 18: Companion Protocol
### Overview
The companion protocol enables BLE (Bluetooth Low Energy) and serial
communication between a MeshCore radio and a companion app (phone, tablet, or
computer). It uses a binary framing format over the Nordic UART Service (NUS)
BLE profile.
### BLE Service
| UUID | Description |
|------|-------------|
| `6E400001-B5A3-F393-E0A9-E50E24DCCA9E` | Nordic UART Service |
| `6E400002-...` | RX Characteristic (app writes to radio) |
| `6E400003-...` | TX Characteristic (radio notifies app) |
### Frame Format
Each companion protocol frame consists of a 1-byte type identifier followed by
variable-length data:
```
[type(1)][data(0-171)]
```
Maximum frame size is 172 bytes.
### Message Types (Radio to App)
| Type | Value | Description |
|------|-------|-------------|
| PACKET_CHANNEL_MSG_RECV | 0x01 | Channel message received |
| PACKET_CONTACT_MSG_RECV | 0x02 | Contact message received |
| PACKET_ADV_RECV | 0x03 | Advertisement received |
| PACKET_CONTACT_MSG_RECV_V3 | 0x04 | Contact message V3 (includes SNR) |
| PACKET_CHANNEL_MSG_RECV_V3 | 0x05 | Channel message V3 (includes SNR) |
### Command Types (App to Radio)
| Command | Value | Description |
|---------|-------|-------------|
| CMD_APP_START | 0x01 | Initialize connection |
| CMD_DEVICE_QUERY | 0x02 | Query device info |
| CMD_SET_CHANNEL | 0x03 | Set active channel |
| CMD_SEND_CHANNEL_MESSAGE | 0x04 | Send message to channel |
| CMD_SEND_CONTACT_MESSAGE | 0x05 | Send message to contact |
| CMD_GET_CONTACTS | 0x06 | Get contact list |
| CMD_GET_CHANNELS | 0x07 | Get channel list |
| CMD_SET_TIME | 0x08 | Set device time |
| CMD_ADD_CONTACT | 0x09 | Add a contact |
| CMD_ADD_CHANNEL | 0x0A | Add a channel |
| CMD_SEND_LOGIN | 0x0B | Login to room/repeater |
| CMD_GET_SETTINGS | 0x0C | Get device settings |
| CMD_SET_SETTINGS | 0x0D | Set device settings |
| CMD_REMOVE_CONTACT | 0x0E | Remove a contact |
| CMD_REMOVE_CHANNEL | 0x0F | Remove a channel |
| CMD_SHARE_CONTACT | 0x10 | Share contact info |
| CMD_SET_ADV_NAME | 0x11 | Set advertisement name |
| CMD_REBOOT | 0x12 | Reboot device |
| CMD_SEND_RAW | 0x13 | Send raw packet |
### Connection Sequence
1. Scan for BLE devices advertising the Nordic UART Service.
2. Connect and discover services.
3. Enable notifications on the TX characteristic.
4. Send `CMD_APP_START` to initialize.
5. Send `CMD_DEVICE_QUERY` to get device info.
6. Send `CMD_SET_TIME` to synchronize clock.
7. Send `CMD_GET_CONTACTS` and `CMD_GET_CHANNELS` to fetch state.
### Byte Order
All multi-byte integers in the companion protocol are **little-endian**, except
for CayenneLPP sensor data which uses **big-endian**.
### Message Length Limit
Text messages are limited to 133 characters.
### MTU Considerations
The default BLE MTU is 23 bytes (20 bytes of payload). For complex operations,
implementations SHOULD request a larger MTU (up to 512 bytes).
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Underlying packet format
- Test vectors: `corpus/companion/`
### Reference Implementation
- `docs/companion_protocol.md` in the MeshCore repository
+180
View File
@@ -0,0 +1,180 @@
# MeshCore Protocol Specification
## Section 19: KISS Modem Protocol
### Overview
The KISS (Keep It Simple, Stupid) modem protocol provides a serial interface to
MeshCore LoRa radios. It follows the KA9Q/K3MC KISS TNC specification with
MeshCore-specific SetHardware extensions for cryptographic operations, radio
control, and telemetry.
### Serial Configuration
- Baud rate: 115200
- Data bits: 8
- Parity: None
- Stop bits: 1
- Flow control: None
### Frame Format
```
┌──────┬───────────┬──────────────┬──────┐
│ FEND │ Type Byte │ Data (escaped)│ FEND │
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
└──────┴───────────┴──────────────┴──────┘
```
### Special Bytes
| Byte | Name | Value | Description |
|------|------|-------|-------------|
| FEND | Frame End | 0xC0 | Frame delimiter |
| FESC | Frame Escape | 0xDB | Escape character |
| TFEND | Transposed FEND | 0xDC | FESC + TFEND represents 0xC0 in data |
| TFESC | Transposed FESC | 0xDD | FESC + TFESC represents 0xDB in data |
### Byte Stuffing
To send a data byte of 0xC0 (FEND), emit: 0xDB 0xDC (FESC TFEND).
To send a data byte of 0xDB (FESC), emit: 0xDB 0xDD (FESC TFESC).
### Type Byte
```
Bit: 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| Port (4) | Command (4) |
+---+---+---+---+---+---+---+---+
```
Port is 0 for single-port TNC (standard for MeshCore).
### Standard KISS Commands (Host to TNC)
| Command | Value | Data | Description |
|---------|-------|------|-------------|
| DataFrame | 0x00 | Raw packet | Queue packet for transmission |
| TXDELAY | 0x01 | 1 byte | Transmitter keyup delay (× 10ms) |
| Persistence | 0x02 | 1 byte | CSMA persistence (0-255) |
| SlotTime | 0x03 | 1 byte | CSMA slot interval (× 10ms) |
| TXtail | 0x04 | 1 byte | Post-TX hold time (× 10ms) |
| FullDuplex | 0x05 | 1 byte | 0=half, nonzero=full |
| SetHardware | 0x06 | Sub-cmd + data | MeshCore extensions |
| Return | 0xFF | — | Exit KISS mode (no-op) |
### TNC to Host
| Type | Value | Data | Description |
|------|-------|------|-------------|
| DataFrame | 0x00 | Raw packet | Received packet from radio |
Data frames carry raw MeshCore packets (up to 255 bytes, MAX_TRANS_UNIT).
### SetHardware Extensions (0x06)
MeshCore extends the KISS protocol via the SetHardware command. The first data
byte is a sub-command identifier.
**Request Sub-commands (Host to TNC):**
| Sub-cmd | Value | Data | Description |
|---------|-------|------|-------------|
| GetIdentity | 0x01 | — | Get node's Ed25519 public key |
| GetRandom | 0x02 | len(1) | Get random bytes (1-64) |
| VerifySignature | 0x03 | pubkey(32)+sig(64)+data | Verify Ed25519 signature |
| SignData | 0x04 | data | Ed25519 sign |
| EncryptData | 0x05 | key(32)+plaintext | AES-128 encrypt |
| DecryptData | 0x06 | key(32)+mac(2)+ciphertext | AES-128 decrypt with MAC |
| KeyExchange | 0x07 | remote_pub(32) | X25519 ECDH |
| Hash | 0x08 | data | SHA-256 hash |
| SetRadio | 0x09 | freq(4)+bw(4)+sf(1)+cr(1) | Set radio parameters |
| SetTxPower | 0x0A | power(1) | Set TX power (dBm) |
| GetRadio | 0x0B | — | Get radio parameters |
| GetTxPower | 0x0C | — | Get TX power |
| GetCurrentRssi | 0x0D | — | Get current RSSI |
| IsChannelBusy | 0x0E | — | Check if channel busy |
| GetAirtime | 0x0F | pkt_len(1) | Estimate air time |
| GetNoiseFloor | 0x10 | — | Get noise floor |
| GetVersion | 0x11 | — | Get firmware version |
| GetStats | 0x12 | — | Get RX/TX statistics |
| GetBattery | 0x13 | — | Get battery voltage |
| GetMCUTemp | 0x14 | — | Get MCU temperature |
| GetSensors | 0x15 | perms(1) | Get sensor data |
| GetDeviceName | 0x16 | — | Get device name |
| Ping | 0x17 | — | Ping |
| Reboot | 0x18 | — | Reboot device |
| SetSignalReport | 0x19 | enable(1) | Enable/disable RxMeta |
| GetSignalReport | 0x1A | — | Get signal report status |
**Response Sub-commands (TNC to Host):**
Response codes: `response = command | 0x80`
| Sub-cmd | Value | Data |
|---------|-------|------|
| Identity | 0x81 | pubkey(32) |
| Random | 0x82 | random_bytes(1-64) |
| Verify | 0x83 | result(1): 0=invalid, 1=valid |
| Signature | 0x84 | signature(64) |
| Encrypted | 0x85 | mac(2)+ciphertext |
| Decrypted | 0x86 | plaintext |
| SharedSecret | 0x87 | secret(32) |
| HashResult | 0x88 | hash(32) |
| Radio | 0x8B | freq(4)+bw(4)+sf(1)+cr(1) |
| TxPower | 0x8C | power(1) |
| CurrentRssi | 0x8D | rssi(1, signed) |
| ChannelBusy | 0x8E | busy(1): 0=clear, 1=busy |
| Airtime | 0x8F | millis(4) |
| NoiseFloor | 0x90 | dBm(2, signed) |
| Version | 0x91 | version(1)+reserved(1) |
| Stats | 0x92 | rx(4)+tx(4)+errors(4) |
| Battery | 0x93 | millivolts(2) |
| MCUTemp | 0x94 | temp(2, signed, tenths °C) |
| Sensors | 0x95 | CayenneLPP data |
| DeviceName | 0x96 | name(UTF-8) |
| Pong | 0x97 | — |
| SignalReport | 0x9A | status(1) |
| OK | 0xF0 | — |
| Error | 0xF1 | error_code(1) |
| TxDone | 0xF8 | result(1): 0=fail, 1=success |
| RxMeta | 0xF9 | snr(1)+rssi(1) |
### Error Codes
| Code | Value | Description |
|------|-------|-------------|
| InvalidLength | 0x01 | Request data too short |
| InvalidParam | 0x02 | Invalid parameter value |
| NoCallback | 0x03 | Feature not available |
| MacFailed | 0x04 | MAC verification failed |
| UnknownCmd | 0x05 | Unknown sub-command |
| EncryptFailed | 0x06 | Encryption failed |
### Unsolicited Events
**TxDone (0xF8)**: Sent after packet transmission. 0x01 = success, 0x00 = fail.
**RxMeta (0xF9)**: Sent after each received data frame. Contains:
- SNR: 1 byte, signed, × 4 for 0.25 dB precision
- RSSI: 1 byte, signed, dBm
### Data Format Notes
- Maximum payload per frame: 255 bytes (MAX_TRANS_UNIT)
- Frames larger than 255 unescaped bytes are silently dropped
- All multi-byte integers are little-endian
- Radio frequency is in Hz (e.g., 869618000)
- Battery voltage in millivolts
- MCU temperature in tenths of °C (e.g., 253 = 25.3°C)
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet format within data frames
- [Section 14: Cryptography](14-crypto.md) — Crypto operations via SetHardware
- Test vectors: `corpus/kiss/`
### Reference Implementation
- `docs/kiss_modem_protocol.md` in the MeshCore repository
+69
View File
@@ -0,0 +1,69 @@
# MeshCore Protocol Specification
## Section 20: Bridge Protocol
### Overview
The bridge protocol enables MeshCore packet forwarding over non-LoRa transports
such as RS232 serial links and ESP-NOW WiFi. It adds a simple framing layer
with a magic number and checksum around the raw MeshCore packet.
### Frame Format
```
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Magic (2B) │ Payload │ Checksum (2B)│ │
│ 0xC03E │ (variable) │ Fletcher-16 │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
```
### Fields
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Magic | 2 bytes | uint16 | `0xC03E` — identifies the frame as a MeshCore bridge packet |
| Payload | variable | raw | Raw MeshCore packet (as produced by `Packet::writeTo()`) |
| Checksum | 2 bytes | Fletcher-16 | Fletcher-16 checksum over the payload |
### RS232 Variant
The RS232 variant adds a length field between the magic and payload:
```
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Magic (2B) │ Length (2B) │ Payload │ Checksum (2B)│
│ 0xC03E │ uint16_le │ (variable) │ Fletcher-16 │
└──────────────┴──────────────┴──────────────┴──────────────┘
```
| Field | Size | Type | Description |
|-------|------|------|-------------|
| Length | 2 bytes | uint16_le | Length of the payload in bytes |
### Fletcher-16 Checksum
The Fletcher-16 checksum is computed over the payload bytes:
```
function fletcher16(data):
sum1 = 0
sum2 = 0
for byte in data:
sum1 = (sum1 + byte) mod 255
sum2 = (sum2 + sum1) mod 255
return (sum2 << 8) | sum1
```
### ESP-NOW Variant
For ESP-NOW transport, the frame format is the same as the basic format (magic +
payload + checksum) without the length field, since ESP-NOW provides its own
length framing.
### Cross-References
- [Section 1: Wire Format](01-wire-format.md) — Packet format within payload
### Reference Implementation
- Bridge handling in MeshCore firmware (board-specific implementations)
+35
View File
@@ -0,0 +1,35 @@
# Summary
[Introduction](00-overview.md)
# Wire Format
- [Wire Format](01-wire-format.md)
- [Header](02-header.md)
- [Path](03-path.md)
# Payloads
- [ACK](04-payload-ack.md)
- [Advertisement](05-payload-advert.md)
- [Encrypted (REQ / RESPONSE / TXT_MSG)](06-payload-encrypted.md)
- [Anonymous Request](07-payload-anon-req.md)
- [Group (GRP_TXT / GRP_DATA)](08-payload-group.md)
- [Path Return](09-payload-path.md)
- [Trace](10-payload-trace.md)
- [Multipart](11-payload-multipart.md)
- [Control](12-payload-control.md)
- [Raw Custom](13-payload-raw-custom.md)
# Cryptography & Identity
- [Cryptography](14-crypto.md)
- [Identity](15-identity.md)
- [Packet Hash](16-packet-hash.md)
# Routing & Higher-Layer Protocols
- [Routing](17-routing.md)
- [Companion Protocol](18-companion-protocol.md)
- [KISS Modem Protocol](19-kiss-protocol.md)
- [Bridge Protocol](20-bridge-protocol.md)