Appearance
Node identity, advertisement, and peer node info
Each device has stable identity for routing and human recognition, and periodically advertises metadata so neighbors can build UI and routing tables.
Wire status:
NODE_ADVERT(0x0A) is Base Profile Required — see Compatibility matrix.NODE_INFO_REQ/NODE_INFO_RSPare Normative Optional. See Payloads for otherpayloadTypevalues.
Names
| Field | Max size | Charset | Usage |
|---|---|---|---|
shortName | 4 bytes | Printable ASCII A–Z, 0–9, -, _ (padded with 0x00) | Compact UI, maps, traceroute lists |
longName | 48 bytes | UTF-8 | Human-readable label; may include spaces |
Rules:
shortNameMUST be unique among direct neighbors with high probability; global uniqueness is best-effort via random suffix if collision detected.longNameMAY duplicate; it is not a routing key.
Identity model: nodeIdentity64 vs nodeId32
LowMesh separates stable identity from compact on-air addresses:
| Concept | Size | Role |
|---|---|---|
nodeIdentity64 | 8 bytes | Stable NVM identity (EUI-64-like or randomly generated at provisioning). Used for provisioning, UI, and long-term trust anchors. |
nodeId32 | 4 bytes | Compact runtime address in the mesh header as srcId / dstId. Chosen for airtime efficiency. |
Canonical nodeId32 is Ed25519-linked (below). nodeIdentity64 remains the stable 8-byte NVM identity for provisioning and UI; it is not the default source of the on-air nodeId32. Firmware stores both in lowmesh::nvm::NodeIdentity — see Embedded storage.
Canonical derivation (Ed25519-linked)
Let pkEd be the device’s 32-byte Ed25519 public key (NVM). Implementations MUST compute:
digestId[0..31] = SHA256( pkEd[0..31] )
nodeId32 = read_u32_le( digestId[0..3] )The same pkEd MUST always produce the same nodeId32. When HAS_ED25519_PUBKEY is set, advertised pkEd MUST match this derivation for the advertised nodeId32 / header srcId. Implementations MUST not pick an arbitrary nodeId32 that breaks that linkage.
Collision handling (normative)
If two different Ed25519 public keys are confirmed to advertise the same nodeId32 on the same active channel/domain (via NODE_ADVERT / NODE_INFO: matching nodeId32, differing pkEd):
- Mark the identity collision (log
DROP_IDENTITY_COLLISION/ contactidentityCollision— diagnostic only; not an on-wire status code). - Do not silently choose an arbitrary replacement
nodeId32. - On the node selected to move (product/fleet policy: typically the later-seen or lower
pkEdlexicographically), generate a new long-term Ed25519 identity keypair. - Recompute
pkEd,nodeId32, and any identity fingerprints that depend on that key. - Advertise the new identity.
- Existing contacts SHOULD treat the change as an identity change requiring appropriate trust handling (TOFU reset, re-verify, or user prompt) — see Contacts and Crypto keys.
Do not invent a salted nodeId32 derivation on this wire revision.
Implementations MUST NOT assume nodeId32 is globally unique outside the active channel/domain.
Provisioned / static identity mode (separate)
Some products MAY assign identity at provisioning time. That is a separate provisioning mode, not a silent exception to the canonical derivation:
- If the device advertises
HAS_ED25519_PUBKEY, provisionednodeId32MUST still equalread_u32_le(SHA256(pkEd)[0..3]). Provision the matching keypair; do not overridenodeId32independently. - If a product stores a static
nodeId32without Ed25519 linkage, it MUST NOT setHAS_ED25519_PUBKEY, and it MUST document that mode in the product manual. Peers cannot verifypkEdlinkage in that mode.
Wire and advert fields
- Mesh header:
srcId/dstId=nodeId32 NODE_ADVERT: includesnodeId32(4 bytes LE). The C++ fielddeviceIdis a legacy/code alias fornodeId32— user-facing protocol docs usenodeId32for the 4-byte on-air identity.
Ed25519 public key on the wire (DM prerequisite)
1:1 DMs require peers to learn each other’s Ed25519 public keys out-of-band of the DM ciphertext itself. NODE_ADVERT / NODE_INFO_* SHOULD expose pkEd so neighbors can populate Contact.peerEd25519PublicKey, set trustState, and run X25519 ECDH + HKDF — see Network key & channel keys and Channels & security.
NODE_ADVERT extension: HAS_ED25519_PUBKEY (advertExtFlags & 0x04)
If set, the ed25519PublicKey field is the first extension block: it follows immediately after the base advert record (after advertExtFlags), before the GPS and time-sync blocks when those bits are also set:
| Field | Size | Description |
|---|---|---|
ed25519PublicKey | 32 | Ed25519 public key (MUST match NVM and SHA256 → nodeId32 derivation) |
Example size with only this extension (no GPS, longNameLen = 4 base = 24): 24 + 32 = 56 bytes. In general: (20 + longNameLen) + 32 when 0x04 is the only extension bit that adds payload.
Firmware reference (lowmesh namespace)
Companion apps and firmware SHOULD use the same camelCase identifiers as the wire tables below. The C++ mirrors live in lowmesh::nvm::NodeIdentity and related structs — see Embedded storage.
NODE_ADVERT inner tail: lowmesh::wire::PAYLOAD_TYPE_NODE_ADVERT, NODE_ADVERT_EXT_* masks, ParsedNodeAdvert, and parseNodeAdvertPayload — see lowmesh/wire/mesh_pdu.hpp.
NODE_INFO / NODE_ADVERT payloads
NODE_ADVERT (0x0A) — “here I am”
Transmitted as periodic mesh frame (often flood with low rate) or piggyback on beacons.
| Field | Size | Description |
|---|---|---|
nodeId32 | 4 | Sender (LE uint32; C++ alias: deviceId) |
shortName | 4 | Packed ASCII |
longNameLen | 1 | 0–48 |
longNameUtf8 | ≤48 | UTF-8 bytes |
role | 1 | Gateway / Repeater / Client bitmask (legacy docs: Edge) |
radioProfileId | 1 | Must match Radio profile |
keyEpoch | 2 | For crypto rotation |
fwMajor | 1 | |
fwMinor | 1 | |
capabilities | 2 | Bitfield: bulletin, IoT ACK, GPS, voice, security caps, etc. — see Security capability bits below |
hwBoardId | 2 | Optional |
advertExtFlags | 1 | Optional extensions — bit flags below; 0 when none |
advertExtFlags bits (normative):
| Bit | Mask | Name | Meaning |
|---|---|---|---|
| 0 | 0x01 | HAS_GPS | GNSS (or companion) fix extension follows — see below |
| 1 | 0x02 | REQUEST_PHONE_GPS | No GNSS; asks a phone app to push a fix (see GPS — phone assist) |
| 2 | 0x04 | HAS_ED25519_PUBKEY | 32-byte Ed25519 public key extension follows — see Ed25519 extension subsection below |
| 3 | 0x08 | HAS_TIME_SYNC | 5-byte absolute-time extension follows — see Time sync extension subsection below |
| 4–7 | reserved | MUST be 0 on TX |
Security capability bits in capabilities (LE uint16):
| Bit | Mask | Name | Meaning |
|---|---|---|---|
| 8 | 0x0100 | SEC_CAP_CCM8 | Supports MIC_MODE = 01 / AES-256-CCM-8 |
| 9 | 0x0200 | SEC_CAP_ADMIN_SIG | Supports signed ADMIN_CMD command chains |
A sender SHOULD use CCM-8 only when the intended receiver is known to support SEC_CAP_CCM8. For broadcast/flood traffic, use CCM-4 unless all intended receivers support CCM-8 or product policy excludes older peers.
Base wire length: 20 + longNameLen bytes (through advertExtFlags inclusive), with 0 ≤ longNameLen ≤ 48. A typical example with longNameLen = 4 yields 24 bytes before extensions.
Extension order (normative): When multiple extension blocks are present, they MUST appear after the base record in this order: (1) HAS_ED25519_PUBKEY (32 bytes) if 0x04 set; (2) HAS_GPS (14 bytes) if 0x01 set; (3) HAS_TIME_SYNC (5 bytes) if 0x08 set. REQUEST_PHONE_GPS (0x02) does not add payload bytes by itself.
Add +32 bytes when HAS_ED25519_PUBKEY is set, +14 when HAS_GPS is set, +5 when HAS_TIME_SYNC is set (subject to mutual exclusion with GPS — see below).
Optional GPS in the same advert (advertExtFlags)
If advertExtFlags & 0x01 (HAS_GPS):
| Field | Size | Description |
|---|---|---|
gpsSource | 1 | 0 = onboard GNSS, 1 = phone / companion (BLE/USB proxy), 2 = last known cached |
fixQuality | 1 | Same semantics as GPS_FIX |
timestampUtc | 4 | Unix epoch seconds (LE) |
latitudeE7 | 4 | signed, degrees × 10⁷ |
longitudeE7 | 4 | signed, degrees × 10⁷ |
If advertExtFlags & 0x02 (REQUEST_PHONE_GPS): device has no GNSS and asks a connected phone app to push a fix (see GPS — phone assist).
Airtime: Including GPS adds 14 bytes when HAS_GPS is set (plus flags byte in base record). Keep advert period conservative.
Implicit time via GPS (HAS_GPS vs HAS_TIME_SYNC)
If HAS_GPS (0x01) is set, the transmitter MUST NOT set HAS_TIME_SYNC (0x08): the GPS extension already carries timestampUtc (absolute Unix epoch seconds). Receiving implementations SHOULD reject adverts that set both 0x01 and 0x08 on the wire.
Trust (normative): Successful decrypt of NODE_ADVERT (including default-community frames under defaultCommunityPsk32) does not make the sender a trusted time authority. Receivers MUST NOT automatically discipline or set their authoritative local/mesh clock from peer HAS_GPS / HAS_TIME_SYNC / GPS_FIX timestamps merely because the MIC verifies. See Time-source trust policy below.
NODE_ADVERT extension: HAS_TIME_SYNC (advertExtFlags & 0x08)
When set (and HAS_GPS is clear), the following 5 bytes are appended after any Ed25519 and/or GPS extension blocks:
| Field | Size | Description |
|---|---|---|
timestampUtc | 4 | LE — sender’s claimed absolute Unix epoch seconds. |
timeStratum | 1 | Sender’s claimed source distance/quality: 0 = sender claims local GNSS / NTP-class input; 1+ = claimed hops from that input (product-defined caps). timeStratum does not grant trust. |
Infrastructure nodes SHOULD include this block only on a paced schedule (see timeSyncAdvertIntervalMinutes in Embedded storage DeviceRole) to limit beacon airtime.
Time-source trust policy (normative)
Peer-advertised GPS/time MUST NOT automatically become another node’s authoritative system/mesh clock.
Trusted authoritative time sources. A node MAY discipline its local/mesh clock from:
- Local onboard GNSS owned by that device (local Stratum-0-quality input).
- A locally paired phone/companion supplying time over the trusted 0-hop BLE/USB relationship.
- A configured trusted Gateway / time authority.
- An authorized admin/control channel.
- A future cryptographically authenticated/signed time-authority mechanism explicitly defined by the protocol.
Ordinary peer timestamps. Timestamps received from arbitrary mesh peers — including NODE_ADVERT GPS timestampUtc, HAS_TIME_SYNC, GPS_FIX, POSITION_CORE, and default-community traffic — MAY be used as peer metadata, event timestamps, freshness estimates, or display information. They MUST NOT automatically become authoritative local clock input unless the peer has been explicitly trusted under product/fleet policy (items 3–5 above).
Stratum semantics. Do not treat an arbitrary peer GPS advertisement as Stratum 0.
- Local GNSS MAY be treated as local Stratum-0-quality input for that device.
- A peer claiming GNSS does not automatically become trusted.
timeStratumis the sender’s claimed/source distance or quality; it does not by itself grant trust.- Trust and stratum are separate concepts.
Security note — malicious or incorrect time injection. The default community domain uses the globally shared defaultCommunityPsk32. Any participant that can produce valid community frames can advertise arbitrary timestampUtc values. An attacker (or a misconfigured node) could push receivers that blindly discipline clocks toward expired certificates, skipped replay windows, wrong bulletin TTLs, or desynchronized IoT ACK sleep. Implementations MUST apply the trust policy above and SHOULD bound/sanity-check untrusted timestamps used only for display or freshness.
NODE_INFO_REQ (0x0B) / NODE_INFO_RSP (0x0C)
Unicast request to a specific nodeId32 to retrieve full record (larger than periodic advert):
Adds (in addition to NODE_ADVERT fields):
| Field | Size | Description |
|---|---|---|
lastGpsE7Lat | 4 | optional 0x80000000 = invalid |
lastGpsE7Lon | 4 | |
uptimeS | 4 | |
batteryMv | 2 | optional; MUST be the calculated absolute millivolts at the battery terminals (post-calibration), not raw ADC counts, not a pre-divider node voltage, and not a “reference-relative” value — independent of ADC reference voltage, divider ratio, or system rail voltage |
batteryPercent | 1 | optional uint8 0–100; 0xFF = unknown/not provided |
Battery UI guidance: If both are present, the UI SHOULD prefer batteryPercent for display. Use batteryMv primarily for diagnostics and logging unless the receiver has chemistry-specific mapping.
Normative measurement note: Firmware MUST apply the full analog front-end model (divider + reference + offset/gain calibration, temperature compensation if used) so batteryMv is comparable across boards. If the hardware cannot determine a meaningful absolute terminal voltage, omit the field / use the wire “unknown” sentinel (0xFFFF where defined).
Receiving other people’s node info
Implementations maintain NeighborTable entries keyed by nodeId32, updated from:
NODE_ADVERT(best for passive discovery),- Data frames (always learn
srcId+ last RSSI/SNR), NODE_INFO_RSP(on-demand detail).
A contacts list merges this with routes, hop counts, traceroute paths, and optional away-message fetches — see Contacts, presence & away message.
UI flow: Map shows longName if known, else shortName, else last-4-hex of nodeId32.
Privacy
- On the default community channel (
channelHash == 0x0000), NODE_ADVERT inner bytes are encrypted; anyone withdefaultCommunityPsk32can decrypt (i.e. effectively all conforming devices). - On encrypted channels, only members decrypt — still consider location fields sensitive.
- Publishing
HAS_ED25519_PUBKEYreveals a stable 32-byte public key (required for DM discovery). Deployments MAY rate-limit or gate this extension on sensitive profiles.
Implementation note (MCU)
Parsing adverts and copying ed25519PublicKey into contact tables is cheap; verifying signatures or running ECDH SHOULD use mbedTLS / TweetNaCl / PSA-style helpers and SHOULD not run synchronously inside radio IRQ contexts on ESP32-S3 or nRF52840.
Example hex (NODE_ADVERT payload only, no GPS extension)
Fabricated: shortName = "Q4A1", longName = "Base" (4 bytes UTF-8), role=repeater 0x02, profile 0, epoch 1, advertExtFlags = 0.
nodeId32 : 08 07 06 05 (LE of 0x05060708)
shortName : 51 34 41 31 ("Q4A1")
longNameLen : 04
longName : 42 61 73 65 ("Base")
role : 02
radioProfile : 00
keyEpoch : 01 00
fwMajor : 01
fwMinor : 04
capabilities : 03 00
hwBoardId : 10 00
advertExtFlags : 00A complete Mesh PDU (current header layout in verFlags VERSION, dynamic ≥16-byte base + this payload) is shown in Packet layout.