Appearance
Full packet layout: PHY checksum, dynamic mesh header, modular tail, AES-CCM
Status: LowMesh has not been publicly released. This is the current mesh specification. The
verFlagsVERSION field (bits 15–14, value0b10) identifies the current LowMesh mesh header layout. The minimum on-air base header is 16 bytes; withHAS_DST_IDit is 20 bytes. Implementations MUST use dynamic parsing (flag-driven offsets), not a single fixed-size C struct for the whole PDU.
This page ties together where each integrity check applies when a frame goes over a LoRa-class radio. The same Mesh PDU bytes are what you log on USB/BLE (after optional HDLC framing).
Layering model
┌──────────────────────────────────────────────────────────────────────┐
│ LoRa modem packet (air) │
│ Preamble │ optional LoRa header │ Mesh PDU bytes │ LoRa CRC16 │
└──────────────────────────────────────────────────────────────────────┘
▲
│ PHYPayload bytes = Mesh PDU (below)
┌──────────────────────────────────────────────────────────────────────┐
│ Mesh PDU (logical bytes passed to RF driver) │
│ dynamicBase │ [regionBlock] │ [fragmentBlock] │ [routeBlock] │ appTail │
└──────────────────────────────────────────────────────────────────────┘| Layer | Integrity | What it proves |
|---|---|---|
| LoRa CRC16 | 2 bytes at end of PHY payload | RF packet not corrupted at PHY. |
| AES-CCM MIC | 4 or 8 bytes after ciphertext (MIC_MODE) | Cryptographic integrity + confidentiality for the application tail. |
| Routing block | Plaintext | Not in CCM AAD — repeaters may extend breadcrumbs without re-encrypting. |
There is no payloadLen byte. Application tail size is derived:
text
prefixBytes = baseBytes + regionBytes + fragmentBytes + routeBytes
appTailLen = rxPhyLen - prefixBytes
micLen = decodeMicLen(verFlags.MIC_MODE) // 4 or 8
cipherLen = appTailLen - micLen // reject if appTailLen < micLen
innerBodyLen = cipherLenSemtech SX1262 PHY ceiling
The entire Mesh PDU MUST fit in ≤ 255 bytes. Originators MUST reserve future route growth for floods — see Routing.
Dynamic base header (VERSION in verFlags)
All multi-byte integers are little-endian unless noted.
verFlags (16-bit, LE)
| Bits | Name | Meaning |
|---|---|---|
| 15–14 | VERSION | 0b10 = current mesh header layout. |
| 13 | HAS_DST_ID | 4-byte dstId present after sessionId. |
| 12 | HAS_REGION | 5-byte Spatial Region block after base. |
| 11 | HAS_FRAGMENT | 2-byte Fragment block after region (if any). |
| 10–9 | HASH_SIZE | Breadcrumb width ((verFlags >> 9) & 3) + 1 bytes (1–4). |
| 8 | FLOOD | Flood forwarding. |
| 7 | FROM_GATEWAY | Gateway-injected. |
| 6 | LOCAL_ONLY | No IP/MQTT bridge. |
| 5 | PRIORITY | Higher queue class. |
| 4 | ACK_REQ | Request ACK_BASIC. |
| 3–2 | MIC_MODE | 00 = CCM-4 (required); 01 = CCM-8 (optional). |
| 1–0 | reserved | MUST be 0 on TX. |
Contiguous base layout (normative order)
| # | Field | Size | Offset (no dstId) | Offset (with dstId) |
|---|---|---|---|---|
| 1 | verFlags | 2 | 0x00 | 0x00 |
| 2 | channelHash | 2 | 0x02 | 0x02 |
| 3 | hopControl / ttlRaw | 1 | 0x04 | 0x04 |
| 4 | srcId | 4 | 0x05 | 0x05 |
| 5 | sessionId | 4 | 0x09 | 0x09 |
| 6 | dstId (optional) | 4 | — | 0x0D |
| 7 | messageId | 2 | 0x0D | 0x11 |
| 8 | payloadType | 1 | 0x0F | 0x13 |
Base sizes: 16 bytes without dstId; 20 bytes with dstId.
sessionId
sessionId is a mandatory 32-bit LE field identifying the sender’s current boot/session epoch.
- MUST change before
messageIdcan repeat under the samesrcIdand key domain. - MUST NOT repeat for the same
srcIdwhile the same channel key may still be in use. - A persisted monotonic boot/session counter seeded at provisioning is RECOMMENDED.
- Random-only generation without collision protection is NOT RECOMMENDED.
- Firmware MAY store this as
bootSessionin NVM; the on-wire name issessionId.
hopControl / ttlRaw (byte @ 0x04)
| Bits | Name | Meaning |
|---|---|---|
| 7 | HAS_ROUTE_BLOCK | Routing block present in modular tail. |
| 6 | reserved | MUST be 0 on TX. |
| 5–0 | HOP_LIMIT | Remaining hops (0–63). |
CCM AAD zero-masks the entire byte at offset 0x04.
Modular tail order
- Optional Region block (5 bytes) if
HAS_REGION - Optional Fragment block (2 bytes) if
HAS_FRAGMENT - Optional Routing block if
ttlRawbit 7 set - Application tail =
ciphertext || MIC
Fragment block (when HAS_FRAGMENT)
16-bit packed word:
- bit 15:
moreFragments - bits 14–10: reserved, MUST be 0
- bits 9–0:
fragmentIndex(0..1023)
Any logical transfer with more than one RF frame MUST set HAS_FRAGMENT on every frame. fragmentIndex MUST be unique within (channelHash, srcId, sessionId, messageId). Maximum totalCount for chunked payloads is 1024. currentIndex MUST be 0..totalCount-1, therefore 0..1023.
For PAYLOAD_BODY_V2, header fragmentIndex MUST equal body currentIndex, and moreFragments MUST equal (currentIndex + 1 < totalCount). Mismatch → reject the fragment/session. fragmentWord is in the CCM nonce — wrapping or reusing the 10-bit index to represent more than 1024 chunks is forbidden on this wire revision.
Routing block
If HAS_ROUTE_BLOCK: pathCount (1 byte) + pathCount × hashSize fingerprint bytes.
Path fingerprints use route digest.
Application tail
Inner plaintext layout is selected by payloadType (visible in base header, not encrypted).
AES-CCM nonce (12 bytes)
The nonce is not transmitted separately — every component is in the authenticated prefix:
text
nonce =
uint32_le(srcId) ||
uint32_le(sessionId) ||
uint16_le(messageId) ||
uint16_le(fragmentWord)fragmentWord: packed Fragment block whenHAS_FRAGMENT, else0x0000.
See Channels & security.
Duplicate / replay suppression
Receivers MUST key duplicate suppression by:
text
(channelHash, srcId, sessionId, messageId, fragmentWord)Logical reassembly sessions use:
text
(channelHash, srcId, sessionId, messageId)TX size validation (originator)
text
baseBytes + regionBytes + fragmentBytes + existingRouteBytes +
futureRouteGrowth + innerBodyLen + micLen <= 255futureRouteGrowth for floods without an initial routing block:
text
futureRouteGrowth = 1 + initialHopLimit * hashSize(with existing routing block: remainingHopLimit * hashSize). See Routing.
USB/BLE capture
Label:
- Dynamic base (16 or 20 bytes) + optional blocks
- Application tail (
ciphertext || MIC)