Skip to content

Payloads (payloadType + inner tail sizing)

LowMesh’s mesh header is documented in Packet layout.

  • payloadType (1 byte, 0–255) lives in the base header and is visible on the air before decryption — see Packet layout — visible payloadType.
  • After the parsed prefix blocks (base, optional region, fragment, routing), the application tail is AES-256-CCM-4 on the wire: ciphertext || 4-byte MIC for all channelHash values (including 0x0000). After decrypt, that tail is the inner body whose layout is selected by payloadType (tables below).

There is no payloadLen field. The on-air application tail length is:

[ \textbf{appTailLen} = \textbf{rxPhyLen} - \textbf{prefixBytes} ]

where prefixBytes = base + region + fragment + routing blocks. After decrypt, innerBodyLen = cipherLen = appTailLen − micLen.

All multi-byte integers below are little-endian unless noted.

Wire status labels

See Base profile — wire status labels and the compatibility matrix.

LabelMeaning
Base Profile RequiredRequired for the Base Interoperability Profile
Normative OptionalStable wire format; optional for the Base Profile
Design TargetNot stable for interop yet
Vendor/PrivatePrivate extension
InformativeNot a mesh wire requirement

Inner body size summary (Base Profile)

Bounds are for the plaintext inner body (before CCM). On-air add 4-byte MIC. innerBodyLen = plaintext length (cipherLen). PHY budget: Payload limits.

payloadTypeWire statusMin innerMax innerLengthFragmentationBase Profile
0x01 TEXT_UTF8Base Profile Required1256variable (1 + utf8Len)optional header fragment blockYes
0x02 POSITION_COREBase Profile Required1313fixednoYes
0x03 TELEMETRY_BASICBase Profile Required88fixednoYes
0x04 PAYLOAD_BODY_V2Normative Optional8PHY-derivedvariableper-chunk + optional HAS_FRAGMENTOptional
0x06 ACK_BASICBase Profile Required1717fixednoYes
0x07 FRAG_NACKNormative Optional1313 + 4×rangesvariablen/a (recovery)Optional
0x0A NODE_ADVERTBase Profile Required20~120+extvariablenoYes
NODE_INFO_*Normative OptionalvariablenoOptional
0x51 GPS_FIXNormative Optional1919fixednoOptional
0x52 GPS_SCHEDULENormative Optional88fixednoOptional (authorized sources only)
0x710x75 BulletinNormative Optionalper opPHY-derivedvariablequery rsp MAY fragmentOptional
0x91/0x92 AdminNormative Optional15+PHY-derivedvariablenoOptional
IOT_UPLINK / IOT_ACKDesign TargetTBDTBDNo

Dispatch rule (normative)

Implementations MUST branch on payloadType first:

payloadTypeNameInner tail interpretation
0x01TEXT_UTF8Base Profile RequiredDirect UTF-8 chat/status string (see Text encoding).
0x02POSITION_COREBase Profile RequiredDirect compact WGS84 fix (maps/dashboards).
0x03TELEMETRY_BASICBase Profile RequiredDirect small device health / RF snapshot for dashboards.
0x04PAYLOAD_BODY_V2Normative OptionalStateless file/data chunk — every packet carries currentIndex, totalCount, chunkLen, and chunkBytes (see below).
0x06ACK_BASICBase Profile RequiredAcknowledgement for ACK_REQ frames — see below.
0x07FRAG_NACKNormative OptionalFragment negative ACK — missing chunk ranges for PAYLOAD_BODY_V2 — see Fragmentation.
0x75BULLETIN_NOTIFYNormative OptionalBulletin host push-to-notify beacon: **channelHash (2 LE) `
0x00, 0x05, 0x080x74, 0x760xFFreserved / vendor / extensionsInterpret per linked pages (admin 0x91 / 0x92, bulletin 0x710x74 and related flows in Bulletin board, traceroute 0x0D / 0x0E, NODE_ADVERT 0x0A, etc.) or treat as opaque if unknown.

Namespaces: Mesh payloadType is not the same as USB/BLE frame opcodes (see USB & BLE hex API); keep parsers separate.

Note: PAYLOAD_BODY_V1 is removed from the normative spec. Flood / source-route path data lives in the routing block when ttlRaw & 0x80 (HAS_ROUTE_BLOCK) is set — see Routing on wire. Use PAYLOAD_BODY_V2 (0x04) — a payload type name, not a public API version — for chunked binary/text transfers.


TEXT_UTF8 (payloadType = 0x01)

FieldSizeDescription
utf8Len10255
utf8utf8LenUTF‑8 text without BOM

Rules:

  • innerBodyLen MUST equal 1 + utf8Len.
  • utf8Len = 0 is allowed (empty message).

POSITION_CORE (payloadType = 0x02)

A compact position record (same numeric semantics as the core of GPS_FIX in GPS, but fixed size for early tooling).

FieldSizeDescription
fixQuality10 = none/unknown, 1 = 2D, 2 = 3D, 3 = RTK (implementation-defined beyond 3)
timestampUtc4Unix epoch seconds (claimed event time; not automatic clock input — see time-source trust)
latitudeE74signed, degrees × 10⁷
longitudeE74signed, degrees × 10⁷

Rules:

  • innerBodyLen MUST be 13.
  • 0x80000000 MAY be used in either coordinate field to mean “invalid / withheld” if a fix is partial (product policy).

TELEMETRY_BASIC (payloadType = 0x03)

FieldSizeDescription
batteryMv2uint16 LE; 0xFFFF = unknown / not provided
rssiDbm1int8 last measured RSSI for this hop’s RX context (implementation defines which interface); 0x80 = unknown
uptimeSec4uint32 LE seconds since boot
flags1Bit 0: charging; bits 17: reserved (0 on TX unless defined later)

Rules:

  • innerBodyLen MUST be 8.

ACK_BASIC (payloadType = 0x06)

Emitted when ACK_REQ is set in verFlags (receiver policy) or to report receive status for a prior frame.

FieldSizeDescription
ackedSrcId4Original sender srcId (LE nodeId32)
ackedSessionId4Original sessionId (LE uint32) from the base header
ackedMessageId2Original messageId (LE uint16)
ackedFragment2Packed fragment word; 0 if not fragmented
status1Status code (table below)
rxRssiDbm1Signed dBm; 0x80 = unknown
rxSnrQ41int8 SNR in quarter-dB units (decodedSnrDb = rxSnrQ4 / 4.0); 0x80 = unknown
ackDelayMs2Delay from RX to ACK TX (LE uint16); always present — use 0xFFFF if unknown or unused

Status codes:

ValueNameMeaning
0x00OKAccepted
0x01duplicateAlready seen (suppressed)
0x02rejected_no_keyCould not resolve channel key / decrypt failed (diagnostic)
0x03rejected_ttlHop budget exhausted / not forwarded
0x04busyReceiver congested
0x05fragment_missingMissing fragment(s) — peer MAY send FRAG_NACK
0x06rejected_policyRole / region / relay policy drop
0x07rejected_replayReplay / duplicate-window rejection

Rules:

  • innerBodyLen MUST be 17 (fixed layout above).
  • rxSnrQ4: Q4 means quarter-dB units, not a product-defined scale. Encode rxSnrQ4 = round(snrDb × 4) as a signed int8. Decode decodedSnrDb = rxSnrQ4 / 4.0. 0x80 (-128 as int8) is unknown, not -32.00 dB. Representable measurements are −31.75 dB … +31.75 dB (0x810x7F). Examples: +10.00 dB → 40, +7.25 dB → 29, 0 dB → 0, −5.00 dB → −20, unknown → 0x80.
  • ACK_BASIC MUST be encrypted/authenticated under the same channel/domain as the message being acknowledged, unless a product profile explicitly defines a management response channel.
  • Works with PAYLOAD_BODY_V2 and legacy multi-frame TEXT_UTF8 when HAS_FRAGMENT is used.

rejected_no_key note: If a receiver cannot resolve the channel key, it usually cannot emit an encrypted ACK_BASIC on that same channel. rejected_no_key is primarily for local diagnostics, default-community fallback policy, or management contexts (USB/BLE, companion) where a valid response channel exists — do not assume such ACKs are always observable on-air.


FRAG_NACK (payloadType = 0x07)

Normative layout for requesting missing PAYLOAD_BODY_V2 chunks. Full field table: Fragmentation.


PAYLOAD_BODY_V2 (payloadType = 0x04)

Stateless chunk envelope for files and large blobs: each radio frame that uses this type carries its own fragment context.

FieldSizeDescription
bodyVersion10x02 for this layout
bodyFlags1MUST be 0 on TX; reserved for future use
fileType10x01 = Text (UTF-8 chunks), 0x02 = JPEG, 0x03 = Firmware binary. 0x00 = reserved / unknown (SHOULD NOT use on TX unless private).
currentIndex20-based chunk index (LE uint16)
totalCount2Total number of chunks (LE uint16, 1–1024)
chunkLen1Number of pure file bytes in this packet (chunkBytes)
chunkByteschunkLenRaw payload bytes

Rules:

  • innerBodyLen MUST equal 8 + chunkLen (fixed 8-byte prefix above).
  • totalCount MUST be 1–1024; currentIndex MUST be 0..totalCount-1 (therefore 0..1023).
  • Every multi-frame PAYLOAD_BODY_V2 transfer MUST set HAS_FRAGMENT. Header fragmentIndex MUST equal currentIndex. Header moreFragments MUST equal (currentIndex + 1 < totalCount). Disagreeing values → reject the fragment/session.
  • Supporting more than 1024 chunks requires a future wire-format/nonce revision and MUST NOT be implemented by wrapping or reusing the 10-bit fragment index.
  • chunkLen = 0 is allowed only if your application defines empty-chunk semantics; otherwise avoid.

Relationship to routing

  • Repeaters are stateless dumb-pipes; forwarding uses ttlRaw, FLOOD, optional routing block path hashes, and duplicate suppression — see Routing.
  • Strict source routes and flood breadcrumbs are carried in the routing block, not inside the AES-CCM ciphertext — see Routing on wire.

See also

LowMeshOS — always open-source mesh protocol documentation