Skip to content

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 verFlags VERSION field (bits 15–14, value 0b10) identifies the current LowMesh mesh header layout. The minimum on-air base header is 16 bytes; with HAS_DST_ID it 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 │
└──────────────────────────────────────────────────────────────────────┘
LayerIntegrityWhat it proves
LoRa CRC162 bytes at end of PHY payloadRF packet not corrupted at PHY.
AES-CCM MIC4 or 8 bytes after ciphertext (MIC_MODE)Cryptographic integrity + confidentiality for the application tail.
Routing blockPlaintextNot 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 = cipherLen

Semtech 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)

BitsNameMeaning
15–14VERSION0b10 = current mesh header layout.
13HAS_DST_ID4-byte dstId present after sessionId.
12HAS_REGION5-byte Spatial Region block after base.
11HAS_FRAGMENT2-byte Fragment block after region (if any).
10–9HASH_SIZEBreadcrumb width ((verFlags >> 9) & 3) + 1 bytes (1–4).
8FLOODFlood forwarding.
7FROM_GATEWAYGateway-injected.
6LOCAL_ONLYNo IP/MQTT bridge.
5PRIORITYHigher queue class.
4ACK_REQRequest ACK_BASIC.
3–2MIC_MODE00 = CCM-4 (required); 01 = CCM-8 (optional).
1–0reservedMUST be 0 on TX.

Contiguous base layout (normative order)

#FieldSizeOffset (no dstId)Offset (with dstId)
1verFlags20x000x00
2channelHash20x020x02
3hopControl / ttlRaw10x040x04
4srcId40x050x05
5sessionId40x090x09
6dstId (optional)40x0D
7messageId20x0D0x11
8payloadType10x0F0x13

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 messageId can repeat under the same srcId and key domain.
  • MUST NOT repeat for the same srcId while 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 bootSession in NVM; the on-wire name is sessionId.

hopControl / ttlRaw (byte @ 0x04)

BitsNameMeaning
7HAS_ROUTE_BLOCKRouting block present in modular tail.
6reservedMUST be 0 on TX.
5–0HOP_LIMITRemaining hops (0–63).

CCM AAD zero-masks the entire byte at offset 0x04.

Modular tail order

  1. Optional Region block (5 bytes) if HAS_REGION
  2. Optional Fragment block (2 bytes) if HAS_FRAGMENT
  3. Optional Routing block if ttlRaw bit 7 set
  4. 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 when HAS_FRAGMENT, else 0x0000.

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 <= 255

futureRouteGrowth for floods without an initial routing block:

text
futureRouteGrowth = 1 + initialHopLimit * hashSize

(with existing routing block: remainingHopLimit * hashSize). See Routing.

USB/BLE capture

Label:

  1. Dynamic base (16 or 20 bytes) + optional blocks
  2. Application tail (ciphertext || MIC)

See Embedded storage, Routing on wire, Channels & security.

LowMeshOS — always open-source mesh protocol documentation