Skip to content

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_RSP are Normative Optional. See Payloads for other payloadType values.

Names

FieldMax sizeCharsetUsage
shortName4 bytesPrintable ASCII A–Z, 0–9, -, _ (padded with 0x00)Compact UI, maps, traceroute lists
longName48 bytesUTF-8Human-readable label; may include spaces

Rules:

  • shortName MUST be unique among direct neighbors with high probability; global uniqueness is best-effort via random suffix if collision detected.
  • longName MAY duplicate; it is not a routing key.

Identity model: nodeIdentity64 vs nodeId32

LowMesh separates stable identity from compact on-air addresses:

ConceptSizeRole
nodeIdentity648 bytesStable NVM identity (EUI-64-like or randomly generated at provisioning). Used for provisioning, UI, and long-term trust anchors.
nodeId324 bytesCompact 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):

  1. Mark the identity collision (log DROP_IDENTITY_COLLISION / contact identityCollision — diagnostic only; not an on-wire status code).
  2. Do not silently choose an arbitrary replacement nodeId32.
  3. On the node selected to move (product/fleet policy: typically the later-seen or lower pkEd lexicographically), generate a new long-term Ed25519 identity keypair.
  4. Recompute pkEd, nodeId32, and any identity fingerprints that depend on that key.
  5. Advertise the new identity.
  6. 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, provisioned nodeId32 MUST still equal read_u32_le(SHA256(pkEd)[0..3]). Provision the matching keypair; do not override nodeId32 independently.
  • If a product stores a static nodeId32 without Ed25519 linkage, it MUST NOT set HAS_ED25519_PUBKEY, and it MUST document that mode in the product manual. Peers cannot verify pkEd linkage in that mode.

Wire and advert fields

  • Mesh header: srcId / dstId = nodeId32
  • NODE_ADVERT: includes nodeId32 (4 bytes LE). The C++ field deviceId is a legacy/code alias for nodeId32 — user-facing protocol docs use nodeId32 for 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:

FieldSizeDescription
ed25519PublicKey32Ed25519 public key (MUST match NVM and SHA256nodeId32 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.

FieldSizeDescription
nodeId324Sender (LE uint32; C++ alias: deviceId)
shortName4Packed ASCII
longNameLen10–48
longNameUtf8≤48UTF-8 bytes
role1Gateway / Repeater / Client bitmask (legacy docs: Edge)
radioProfileId1Must match Radio profile
keyEpoch2For crypto rotation
fwMajor1
fwMinor1
capabilities2Bitfield: bulletin, IoT ACK, GPS, voice, security caps, etc. — see Security capability bits below
hwBoardId2Optional
advertExtFlags1Optional extensions — bit flags below; 0 when none

advertExtFlags bits (normative):

BitMaskNameMeaning
00x01HAS_GPSGNSS (or companion) fix extension follows — see below
10x02REQUEST_PHONE_GPSNo GNSS; asks a phone app to push a fix (see GPS — phone assist)
20x04HAS_ED25519_PUBKEY32-byte Ed25519 public key extension follows — see Ed25519 extension subsection below
30x08HAS_TIME_SYNC5-byte absolute-time extension follows — see Time sync extension subsection below
4–7reservedMUST be 0 on TX

Security capability bits in capabilities (LE uint16):

BitMaskNameMeaning
80x0100SEC_CAP_CCM8Supports MIC_MODE = 01 / AES-256-CCM-8
90x0200SEC_CAP_ADMIN_SIGSupports 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):

FieldSizeDescription
gpsSource10 = onboard GNSS, 1 = phone / companion (BLE/USB proxy), 2 = last known cached
fixQuality1Same semantics as GPS_FIX
timestampUtc4Unix epoch seconds (LE)
latitudeE74signed, degrees × 10⁷
longitudeE74signed, 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:

FieldSizeDescription
timestampUtc4LE — sender’s claimed absolute Unix epoch seconds.
timeStratum1Sender’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:

  1. Local onboard GNSS owned by that device (local Stratum-0-quality input).
  2. A locally paired phone/companion supplying time over the trusted 0-hop BLE/USB relationship.
  3. A configured trusted Gateway / time authority.
  4. An authorized admin/control channel.
  5. 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.
  • timeStratum is 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):

FieldSizeDescription
lastGpsE7Lat4optional 0x80000000 = invalid
lastGpsE7Lon4
uptimeS4
batteryMv2optional; 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
batteryPercent1optional 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 with defaultCommunityPsk32 can decrypt (i.e. effectively all conforming devices).
  • On encrypted channels, only members decrypt — still consider location fields sensitive.
  • Publishing HAS_ED25519_PUBKEY reveals 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 : 00

A complete Mesh PDU (current header layout in verFlags VERSION, dynamic ≥16-byte base + this payload) is shown in Packet layout.

LowMeshOS — always open-source mesh protocol documentation