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:
24 files changed
+2478
-2
No files matched your search
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
This project follows a loose [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||
convention. Released versions live as frozen snapshots under `versions/vX.Y.Z/`
|
||||
in the repo; the rolling tip of `main` is what `latest` deploys.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
_(nothing yet)_
|
||||
|
||||
## [v0.1.0] — 2026-05-12
|
||||
|
||||
First cut release of the spec. Snapshotted as `versions/v0.1.0/`, deployed
|
||||
at <https://swaits.github.io/meshcore-spec/v0.1.0/>.
|
||||
|
||||
The dated entries below describe the substantive passes whose output is
|
||||
captured in this release. Future releases will follow the standard
|
||||
Added / Changed / Fixed / Removed grouping under their version heading.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-12 — Publication-Readiness Pass
|
||||
|
||||
**Upstream baseline:** Validated against
|
||||
|
||||
@@ -4,8 +4,9 @@ A comprehensive specification and conformance test suite for the
|
||||
[MeshCore](https://github.com/meshcore-dev/MeshCore) mesh networking protocol.
|
||||
|
||||
**📖 Read the spec online:** <https://swaits.github.io/meshcore-spec/>
|
||||
(rolling `latest` build, plus a frozen snapshot per `vX.Y.Z` tag — pick one
|
||||
from the version-picker in the top-right of any page)
|
||||
(rolling `latest` from `main`, plus a frozen snapshot per release —
|
||||
current: [v0.1.0](https://swaits.github.io/meshcore-spec/v0.1.0/). Use
|
||||
the version-picker in the top-right of any page to switch.)
|
||||
|
||||
MeshCore Spec aims to be the authoritative, machine-readable definition of the
|
||||
MeshCore wire format — much like [RubySpec](https://github.com/ruby/spec) became
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user