Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

UMSH is an experimental LoRa-oriented mesh protocol that grew out of a simple question: what would a cryptographically addressed LoRa mesh look like if designed from the ground up with strong security and clean architecture? Inspired by MeshCore, UMSH started as a thought experiment addressing what its author saw as critical shortcomings—shortcomings that would practically require backward-incompatible changes to properly fix. What began as a toy protocol has since developed into this comprehensive specification.

Few, if any, of the ideas presented here are new or exotic. Many come directly from MeshCore—cryptographic addressing, source routing, regions, and more. The key contribution here is a clean, extensible design with stronger cryptography and clear layer separation.

This specification was written with MeshCore V2 in mind, but the ideas are free for anyone to adopt. Meshtastic has discussed the possibility of a breaking v3 revision, and some of these ideas may be relevant there as well. Point-by-point protocol comparisons with MeshCore, Meshtastic, and Reticulum are available in the appendices.

The project repository, reference implementation, and supporting tooling are available on GitHub.

Overview

In UMSH, endpoints are identified by public keys, and multicast communication is based on shared symmetric channel keys. At its core, UMSH defines a MAC layer with framing, addressing, encryption, authentication, and hop-by-hop forwarding. UMSH also defines application-layer protocols for text messaging, chat rooms, and node management that are built on top of this foundation. The MAC layer treats payloads opaquely and can equally carry UMSH-defined application protocols, third-party protocols such as CoAP, or any other higher-layer content.

UMSH is designed to support:

  • Public-key-addressed unicast
  • Symmetric-key multicast
  • Optional payload encryption
  • End-to-end authentication across multi-hop paths
  • Selective flooding and source-routed delivery
  • Operation in both encrypted and amateur-radio-compliant unencrypted modes
  • Ability to operate in a way that preserves perfect forward secrecy (PFS)
  • Timestamp-free at the MAC layer

Design Principles

The following principles guide UMSH’s design. When evaluating a design decision, prefer the option that best satisfies these principles. The order of these principles shouldn’t be interpreted as an order of importance.

Single-frame design. Every packet must fit in a single LoRa frame. Operations that require more data than one frame can carry belong at a higher layer.

Confidentiality. Encryption and authentication must be available for every kind of interaction. Where metadata concealment is also needed, the protocol should provide opt-in mechanisms rather than requiring it universally.

Robustness. Prefer constructions that fail gracefully over ones that fail catastrophically. A slightly less efficient design that degrades safely under adverse conditions is better than an efficient one that breaks badly.

Tolerance of loss and disorder. Assume packets will be dropped, duplicated, and delivered out of order. No operation should require synchronized state between two nodes that cannot recover from a missed message.

Brevity. Every byte costs airtime. Mandatory fields should be as small as correctness allows; optional fields should be absent when not needed.

Practicality. A protocol must be operable, not just correct. Prefer designs that make real networks easier to understand and debug—readable packet traces, attributable traffic, and identifiable nodes are operational requirements, not luxuries. Theoretical minimalism that makes a protocol difficult to deploy or troubleshoot is a real cost.

Layer separation. The MAC layer routes and delivers opaque payloads. It must not depend on payload content, and payload protocols must not depend on MAC-layer internals.

Minimal mandatory state. Basic operation should require only a node’s own keypair and its configured channel keys. Path tables, clock synchronization, and session state are optional enhancements, not prerequisites.

Graceful extensibility. The protocol must be able to evolve without requiring coordinated upgrades across a deployed network. New features should be deployable incrementally, with older nodes degrading gracefully rather than failing.

Note

Regarding layer separation… There is a case to be made that having the payload type be non-opaque could be a benefit to the mesh as a whole, because you could then do things like prioritize text chat messages over other types of messages at the repeater level. This would help to ensure that there is a minimum-level of text service always available, even if there is lots of other traffic present. I am not yet convinced this is a good idea yet, but it is worth thinking about.

Use Cases

UMSH is designed for deployments where LoRa’s range and low power consumption are valuable and where the constraints of LoRa—low data rates, small frame sizes, shared channel—make protocol efficiency and cryptographic robustness important.

Intended use cases include:

  • Off-grid text communication—chat, direct messaging, and group channels between people in areas without cellular coverage: hiking, expeditions, disaster response, rural communities.
  • Emergency and disaster communications—resilient mesh networking that operates without any fixed infrastructure and degrades gracefully as nodes go offline.
  • IoT and sensor telemetry—authenticated sensor readings from battery-powered field devices, where per-packet overhead directly affects battery life and where tampered readings could have real consequences.
  • Amateur radio mesh networking—the protocol defines explicit amateur-radio-compliant modes with callsign fields and mandatory unencrypted operation, supporting legal use on amateur frequencies.
  • Privacy-sensitive communication—blind unicast and encrypted multicast allow metadata concealment (sender and recipient identity) for contexts where traffic analysis is a concern.
  • Embedded and constrained deployments—compact encoding (1-byte FCF, compact address hints, minimal per-packet overhead), single-frame design, and no mandatory runtime state (no path tables, no clock synchronization) make UMSH suitable for bare-metal microcontrollers with minimal RAM and no operating system.

UMSH is not designed for:

  • High-bandwidth applications—LoRa data rates (typically 0.3–27 kbps) make real-time voice, video, or large file transfer impractical.
  • Applications requiring low latency—multi-hop flood delivery adds variable latency that makes UMSH unsuitable for interactive or time-sensitive protocols.

Key Concepts

Nodes

A node is a logical endpoint on the network, identified by a 32-byte Ed25519 public key. That public key is the node’s long-term network identity—it serves as both its address and its cryptographic credential. A single physical device may host multiple nodes (e.g., a repeater node and a chat node), each with its own keypair.

A node’s public key must be known to communicate with it directly. Public keys can be learned through several mechanisms:

  • Beacons and advertisements—nodes periodically broadcast their presence, optionally including identity information (see Node Identity)
  • QR codes and URIs—public keys can be shared out-of-band via umsh:n: URIs (see URI Formats)
  • First-contact packets—a sender can set the S flag to include its full public key in any packet, allowing the receiver to learn it directly from the wire

Once a node’s public key is known, it can be cached and subsequent packets can use a compact source hint instead of the full key—saving 29 bytes per packet in unicast (3-byte hint vs 32-byte key).

Node Metadata

Nodes may also advertise additional metadata—such as a human-readable name, role, capabilities, and location—via the Node Identity payload. Metadata can also be contained in QR codes and URIs. This metadata is carried at the application layer and is not required by the MAC layer.

Unicast

Unicast packets are addressed to a destination node and may be authenticated or encrypted using per-destination cryptographic material derived from sender/recipient key agreement (see Frame Types and Security & Cryptography).

Channels

A channel is a shared symmetric key that serves two roles: multicast group communication and blind unicast metadata concealment. All nodes configured with a given channel key are members and can send and receive multicast packets addressed to it. Blind unicast uses the channel key to hide sender and destination addresses on the wire, while protecting the payload with combined keys that require both the channel key and the pairwise shared secret—only the intended recipient can read it. See Channels for channel types, membership models, and default channels.

Perfect Forward Secrecy

UMSH supports perfect forward secrecy, not as a core part of the underlying protocol but instead via ephemeral node addresses. Either node can initiate a PFS session, after which both parties communicate using ephemeral node addresses whose private keys are never stored durably and are erased at session end. Compromise of long-term keys cannot retroactively expose traffic protected by a completed PFS session.

Definitions

The following terms are used throughout this specification. Definitions are given in the context of UMSH; terms with broader meanings in other fields are defined here as they apply to this protocol.

Address
A node’s 32-byte Ed25519 public key, used as its stable network identifier. UMSH addresses are not numeric values used for routing. Short address hints derived from the address are used on the wire to save space.
Address Hint
A short prefix of a node’s public key used as a cheap prefilter before full cryptographic processing. Source and destination hints are 3 bytes; router hints, as used in source routes and traces, are 2 bytes. See Addressing.
AES (Advanced Encryption Standard)
A symmetric block cipher standardized by NIST. UMSH uses AES-256: AES-CMAC-based S2V for message authentication and AES-CTR for payload encryption.
AES-SIV (Synthetic Initialization Vector)
A misuse-resistant authenticated encryption scheme defined by RFC 5297. It uses S2V to compute a synthetic IV from the associated data and the plaintext, then encrypts with AES-CTR. UMSH secures packets with AES-SIV using AES-256, extended with MIC truncation and a SECINFO-padded CTR IV; with a full 16-byte MIC the construction is byte-for-byte AEAD_AES_SIV_CMAC_512. See Encrypted Packets.
ARNCE/HAM-64
A compact character encoding scheme for amateur radio callsigns, encoding up to 12 characters into 2, 4, 6, or 8 bytes. Used in UMSH’s Operator Callsign and Station Callsign packet options.
Beacon
A broadcast or multicast packet with an empty payload. Beacons advertise a node’s presence and are used for path discovery and route learning. See Beacons & Path Discovery.
Blind Unicast
A packet type that carries a unicast payload addressed to a specific destination while concealing both the sender and destination identities from observers who do not possess the channel key. The destination hint and source address are encrypted using the channel key; the payload is protected end-to-end using keys derived from both the channel key and the pairwise shared secret. See Frame Types.
Bridge
An arrangement that carries UMSH packets between two different media or channels—for example, from a local LoRa radio to an internet backhaul and back to a distant LoRa radio. Bridges are protocol-transparent: a repeater at each end forwards the packet as it would any other, so a crossing consumes source-route hints, spends two flood hops, and records both repeaters in a trace. The inbound repeater’s forwarding is observable on the medium the packet arrived from.
Broadcast
A packet intended for all nodes in range. Broadcast packets carry no destination hint and are not encrypted or authenticated at the MAC layer. See Frame Types.
CAD (Channel Activity Detection)
A LoRa physical-layer feature that listens briefly for preamble energy on the channel with minimal power draw. UMSH uses CAD to implement listen-before-talk channel access, reducing collisions without requiring continuous reception. See Channel Access.
Channel
A logical communication group secured by a shared symmetric key. Channels serve two purposes: group communication (multicast) and metadata concealment (blind unicast). See Channels.
CoAP (Constrained Application Protocol)
A lightweight request/response protocol designed for constrained networks (RFC 7252). UMSH borrows CoAP’s delta-length option encoding for packet options and application-layer payloads.
Confidentiality
The property that payload content is accessible only to intended recipients. UMSH provides confidentiality via AES-256 encryption keyed with material derived from ECDH (unicast) or the channel key (multicast).
Duplicate Suppression
A mechanism by which repeaters track recently forwarded packets and decline to forward the same packet a second time. Each packet is identified by its MIC (for authenticated packets) or a locally computed hash (for unauthenticated packets). This prevents flood-routed packets from circulating indefinitely. See Repeater Operation.
ECDH (Elliptic Curve Diffie-Hellman)
A key agreement protocol that allows two parties to derive a shared secret using only their public keys and their own private keys. UMSH uses X25519 ECDH to derive pairwise shared secrets for unicast encryption and authentication.
Ed25519
An elliptic curve digital signature algorithm using the Edwards25519 curve. UMSH uses Ed25519 keypairs as node identities. The same keypair is converted to X25519 form for key agreement.
EdDSA (Edwards-curve Digital Signature Algorithm)
The family of signature algorithms that includes Ed25519. Used in UMSH for frame signatures, which attribute a frame to its sender’s key, and for the optional signature that lets a node identity stand on its own outside a frame.
Ephemeral Key
A temporary Ed25519 keypair generated for a single PFS session. Unlike a long-term identity keypair, ephemeral keys are never written to persistent storage and are explicitly erased when the session ends, ensuring that compromise of long-term keys cannot retroactively decrypt traffic protected by them. See Security & Cryptography.
Flood Hop
A hop spent from FHOPS_REM: a transmission made with FHOPS_REM > 0 and no source-route hint left to follow, which is what permits the repeaters that hear it to flood-forward the packet. The final transmission of a flood-routed packet is made with FHOPS_REM = 0 and is a hop but not a flood hop, and a source-routed hop is not a flood hop either. A path’s hop count is its source-routed hops plus its flood hops plus one, so it is one more than FHOPS_ACC if and only if no source route was involved. See Flood Hop Count.
Flood Routing
A routing strategy where a packet is forwarded by every eligible repeater within the flood radius, subject to duplicate suppression. Requires no topology state at repeaters. Bounded by the flood hop count field (FHOPS). See Repeater Operation.
Frame
The unit of transmission at the LoRa PHY layer. A UMSH packet must fit within a single frame. The terms frame and packet are used interchangeably in this specification; frame emphasizes the physical transmission unit, packet emphasizes the logical protocol unit.
Frame Counter
A monotonically increasing 4-byte value included in every authenticated packet. The receiver tracks recently seen counter values and rejects packets with counters it has already processed, providing replay protection without requiring synchronized clocks. The counter must be persisted across reboots to prevent reuse. See Security & Cryptography.
Hop
One leg of a packet’s path through the network—the transmission from one node to an adjacent node within radio range. A packet that travels through two repeaters before reaching its destination has traversed three hops. A zero-hop message is a message sent to yourself that is never sent over the radio. A hop taken by a repeater matching a source-route hint is a source-routed hop and spends no FHOPS budget; a hop spent from FHOPS_REM is a flood hop. Between them the two counts leave exactly one transmission uncounted, so a path always has one more hop than the two counts together.
HKDF (HMAC-based Key Derivation Function)
A key derivation function standardized in RFC 5869, composed of two steps: Extract (combining a secret and optional salt into a pseudorandom key) and Expand (stretching that key to the required output length). UMSH uses HKDF-SHA256 with domain-separated labels to derive encryption keys, authentication keys, and channel identifiers from shared secrets and channel keys.
IoT (Internet of Things)
A broad term for networked embedded devices. UMSH is designed to be applicable to IoT use cases in addition to human communication, though it is optimized for the latter.
Latency
The time elapsed between a packet being transmitted and its receipt or acknowledgement. LoRa’s low data rate and mesh forwarding introduce significant latency compared to IP networks.
LoRa (Long Range)
A proprietary wireless modulation technology using chirp spread spectrum, designed for long range and low power consumption at the cost of low data rate. LoRa payloads are typically limited to 200–250 bytes, and transmitting a single packet may take hundreds of milliseconds depending on spreading factor and bandwidth. UMSH is designed around these constraints.
Long-Term Identity
A node’s stable Ed25519 keypair, used as its persistent network identity. Contrast with ephemeral keypairs used in PFS sessions, which are discarded after use.
MAC Layer (Medium Access Control)
The sublayer of the data link layer responsible for addressing, packet framing, and channel access. In UMSH, the MAC layer handles packet types, addressing, routing options, and cryptography. Application-layer protocols are carried in the payload and are defined separately.
MAC Ack
A packet type generated by the final destination of a unicast packet (UNAR or BUAR) to confirm receipt. The MAC Ack carries an ack MIC—the first 4 bytes of the original packet’s MIC, a public handle that lets the sender and any forwarding repeater correlate the ack with the original packet—and a 4-byte ack tag, a MIC over the ack’s static options bound to the original packet, which allows the original sender to verify the acknowledgement. It carries no destination hint. Repeaters do not generate MAC Acks; they forward them like any other packet. See Frame Types.
Mesh
A network topology where nodes can relay packets on behalf of other nodes, enabling communication beyond direct radio range. UMSH is designed for LoRa mesh networks where repeaters and bridges are used to extend coverage.
MIC (Message Integrity Code)
UMSH’s authentication tag, the truncated S2V output (RFC 5297) computed over the packet’s static fields and payload. Size is configurable from 4 to 16 bytes. The term MIC is used instead of the more common “MAC” (Message Authentication Code) to avoid confusion with Medium Access Control. See Security & Cryptography.
Multicast
A packet intended for all members of a channel, identified by a 2-byte channel identifier derived from the channel key. See Frame Types.
Node
A logical participant in a UMSH network, defined by a unique Ed25519 keypair. A single physical device may host multiple nodes. A node may act as an endpoint (sending and receiving application data), a repeater (forwarding packets), or both.
Packet
The logical unit of the UMSH protocol. Every packet must fit within a single LoRa frame. See Frame above.
Pairwise Key
A symmetric key derived from X25519 ECDH between a specific sender and recipient. Each pair of nodes shares a unique set of pairwise keys (one for encryption, one for authentication) derived deterministically from their long-term key material. Pairwise keys are stable across sessions unless a PFS session is used. See Security & Cryptography.
PFS (Perfect Forward Secrecy)
A property ensuring that compromise of long-term keys does not allow retroactive decryption of past traffic. UMSH provides optional PFS via ephemeral keypair sessions. See Security & Cryptography.
PHY (Physical Layer)
The lowest layer of a network stack, responsible for modulation and transmission over the physical medium. In UMSH deployments, this is typically LoRa. The PHY layer is below the MAC layer and is not defined by this specification.
Private Key
The secret half of an asymmetric keypair. In UMSH, a node’s Ed25519 private key is used to derive its X25519 private key for ECDH and to sign payloads when required. Must never be transmitted or disclosed.
Privacy
Broader than confidentiality: the protection of metadata and communication patterns in addition to payload content. UMSH provides confidentiality but does not fully protect against traffic analysis. See Security Considerations.
Public Key
The public half of an asymmetric keypair, which can be freely shared. In UMSH, a node’s Ed25519 public key is its address. Address hints are derived from it.
RSSI (Received Signal Strength Indicator)
A measurement of received radio signal power, expressed in dBm (negative values; higher is stronger).
Repeater
A node that forwards packets to extend the effective range of the mesh. Repeaters participate in flood routing and source routing but do not generate or consume application payloads for the packets they forward. See Repeater Operation.
SNR (Signal-to-Noise Ratio)
A measurement of the ratio between received signal power and background noise, expressed in dB. Unlike RSSI, SNR remains a reliable indicator of link quality even when the signal is below the noise floor, which is common in LoRa’s spread-spectrum operating regime.
Source Routing
A routing strategy where the sender specifies the explicit sequence of repeaters a packet must traverse. Requires the sender to have prior knowledge of a valid path. See Packet Options.
Trace
A packet option that instructs each forwarding repeater to prepend a record of itself—its router hint and the signal it received the packet with—to the option value, building an ordered record of the path a packet has taken (most-recent repeater first). Used for route learning: a destination that receives a packet with a complete trace has enough information to construct a source route for the reply. See Packet Options.
Unicast
A packet addressed to a single destination node, identified by a 3-byte destination hint. Unicast packets are always authenticated end-to-end; encryption is optional and controlled by the E flag. See Frame Types.
URI (Uniform Resource Identifier)
A compact string that identifies a resource or address. UMSH defines URI schemes for addressing nodes and channels. See URI Formats.
X25519
An elliptic curve Diffie-Hellman function over Curve25519. UMSH converts Ed25519 keypairs to X25519 form for key agreement. The conversion is deterministic and well-defined.

Common Patterns

UMSH reuses a small set of encoding patterns across both the MAC layer and application-layer protocols. This chapter collects them in one place so that individual protocol sections can reference them without repeating the details.

Byte Order

Multi-byte numeric fields are transmitted in big-endian (most-significant byte first), also known as network byte order. This is the standard convention for protocol specifications and is straightforward to interpret in packet diagrams. Modern hardware can convert between byte orders at effectively zero cost, so this choice imposes no practical performance penalty.

Non-numeric multi-byte data—such as SHA-256 hashes, Ed25519 public keys, and EdDSA signatures—is transmitted in its most common byte-wise representation, independent of any underlying endianness of the represented value.

CoAP-Style Option Encoding

UMSH uses the delta-length option encoding defined in CoAP (RFC 7252 §3.1) wherever a set of typed key-value fields needs to be carried compactly. This allows the protocols to be flexible and adapt to future needs while retaining backward compatibility.

Each option is encoded as a delta from the previous option’s number, a length, and a value. The sequence is terminated by a 0xFF end-of-options marker when a PAYLOAD follows the options block; packets with no payload omit the marker.

This encoding appears in:

  • MAC-layer packet options—routing, signal-quality thresholds, callsigns (see Packet Options)
  • Text message options—message type, sender handle, fragmentation, colors (see Text Messages)
  • Chat room payloads—room info responses, login parameters (see Chat Rooms)
  • Node identity metadata—name, location, altitude, timestamp, supported regions (see Node Identity)

The full encoding rules—nibble interpretation, extended bytes, and the end marker—are defined in Packet Structure. Application-layer uses follow the same wire format.

ARNCE/HAM-64 Text Encoding

ARNCE (Amateur Radio Numeric Callsign Encoding), also known as HAM-64, is a compact encoding for short alphanumeric strings. It packs up to 12 characters into 2, 4, 6, or 8 bytes, making it well suited for identifiers that must fit in constrained fields.

UMSH uses ARNCE/HAM-64 for:

  • Operator callsign (packet option 4)—identifies the originating operator under amateur radio rules
  • Station callsign (packet option 15)—identifies the transmitting station, updated by repeaters during forwarding
  • Region codes (packet option 11)—short codes of one to three letters or digits encoded as 2-byte ARNCE values (e.g. SJC → 0x7853)

UTF-8 Strings

All human-readable text in UMSH—message bodies, node names, sender handles, room descriptions—is encoded as UTF-8. String length is determined by context:

  • Inside a CoAP-style option, the option’s length field defines the string boundary.
  • As trailing data after a 0xFF marker, the string extends to the end of the payload (or to the start of a trailing signature).

A sender that shortens a string to fit a byte limit (a node name, a sender handle, a region name) cuts it at an extended grapheme cluster boundary (UAX #29), so that an emoji sequence or a letter with combining marks is kept whole or dropped whole. A receiver given a string that ends mid-cluster renders what it can; it does not reject the string. Message fragmentation splits bodies at byte boundaries and is unaffected (see Text Messages).

Base58 Encoding

Public keys and channel keys in human-facing contexts (URIs, QR codes) are encoded using Base58. This avoids visually ambiguous characters (0/O, l/1) and produces compact, copy-paste-friendly strings.

See URI Formats for the defined URI schemes.

Addressing

UMSH nodes are identified by their 32-byte Ed25519 public keys. Including a full 32-byte address in every packet would be expensive in the constrained LoRa frame budget, so UMSH defines several compact hint representations—short prefixes of a public key that allow receivers to quickly identify likely matches without the full key. Hints are not cryptographically authoritative; they serve only as cheap prefilters to avoid unnecessary work.

The sections below describe the three addressing forms used across the protocol: node hints, router hints, and source addresses.

Presentation

UMSH addresses are canonically encoded and displayed using base 58. Addresses may also be rendered using base 16 as 64 hexadecimal digits—lowercase preferred, either case accepted when parsing—but the preferred encoding is base58. The two forms are unambiguously distinguished by length.

The base58 encoding is fixed-length: a 32-byte address always renders as exactly 44 digits, left-padded with 1 (the base58 zero digit) when its numeric value would otherwise encode shorter.

Node Hint

A node hint is defined as:

  • the first three bytes of a node’s 32-byte public key

Node hints serve as the destination hint in unicast packets and as the compact source hint in source addresses (see below). A node hint is not authoritative and is used only as a fast prefilter to avoid unnecessary cryptographic work.

A receiver that sees a matching destination hint must still confirm that it is the intended destination by successfully processing the packet cryptographically.

Rendering Node Hints

Rendering a node hint to the user is a little tricky due to the fact that most node hints can be rendered as just the first four characters of the encoded public key. In these cases, we would simply take our node hint, append 29 zeros, perform the base 58 encoding, and just drop everything except the first four characters.

However, in some cases when using this method, the fourth character may differ from the fourth character in the actual address. This is highly undesirable.

One way to address this is to perform two base58 encodings: one padded with 0x00 and one padded with 0xFF. If the fourth encoded character is the same between them both, then four characters can be used as the rendered node hint. However, if the encodings differ, the rendered hint is the longest common prefix of the two encodings followed by a single *. The common prefix is three characters except in the rare case where a carry propagates into the third digit, leaving two verified characters. Every character preceding the * is guaranteed to match the full base58 rendering of any matching address.

Note that there are more efficient ways of calculating this than padding with 0x00 and 0xFF and encoding the whole address twice, but logically that is the process.

Router Hint

A router hint is defined as:

  • the first two bytes of the repeater’s 32-byte public key

Router hints are used in:

  • source-route options
  • trace options

Because router hints are only 2 bytes, collisions are possible in dense networks but are handled gracefully: the MIC-based duplicate suppression ensures that each repeater forwards a given packet at most once, so a router hint collision causes a spurious forward but not a loop or incorrect delivery.

Rendering Router Hints

Router hints are rendered using the same procedure as node hints, with a three-character budget and 30 bytes of padding. Because two bytes pin down only slightly more than two base58 digits, the third character is ambiguous for most router hints; the two-characters-plus-* form is the common case.

Source Address

A source address in a packet is either:

  • a compact source hint (a prefix of the sender’s 32-byte public key), when the S flag in the FCF is clear, or
  • the full 32-byte public key, when the S flag is set.

The source hint is 3 bytes (the first three bytes of the public key) when S is clear.

The source hint is a compact reference used when the receiver is expected to already have the sender’s full public key cached (e.g., from a prior advertisement or first-contact exchange). When the full public key is present, the receiver can perform ECDH directly from the packet without any prior state.

In encrypted multicast and blind unicast packets, the source address is carried inside the ciphertext: a 3-byte hint when S is clear, or the full 32-byte public key when S is set.

Packet Structure

All UMSH packets begin with a one-byte Frame Control Field (FCF). Optional common fields then follow in a fixed order, followed by packet-type-specific fields.

Top-Level Packet Layout

+-----+-------+----------+---------+-----------+---------+---------+-------+-----+
| FCF | FHOPS | DST/CHAN |   SRC   |  SECINFO  | OPTIONS | PAYLOAD | [SIG] | MIC |
+-----+-------+----------+---------+-----------+---------+---------+-------+-----+
  1 B   0/1 B    0/2/3 B   0/3/32 B   0/5/7 B   variable  variable  0/64 B  0-16 B

Where:

  • FHOPS is present if the FCF flood hop count flag is set
  • DST is a 3-byte destination hint (MAC Ack packets carry no DST)
  • CHAN is a 2-byte channel identifier
  • SRC is a compact 3-byte source hint (when S flag is clear) or 32-byte source public key (when S flag is set); in multicast and blind unicast packets, SRC follows the options rather than preceding them, and with encryption enabled it is encrypted inside the ciphertext
  • SECINFO is present on authenticated/encrypted packet types
  • SIG is a 64-byte frame signature, present when the SCF’s D bit is set; with encryption enabled it is encrypted with the payload
  • MIC is present on authenticated/encrypted packet types; MAC acks carry a fixed ack trailer (ack MIC + ack tag) instead

A broadcast carries its frame signature, if any, in the Frame Signature option instead of SIG.

Not all fields are supported for all packet types.

Frame Control Field

The Frame Control Field is one byte:

  7   6   5   4   3   2   1   0
+-------+-----------+---+---+---+
|  VER  | PKT TYPE  | S | R | H |
+-------+-----------+---+---+---+
 2 bits    3 bits     1   1   1

Where:

  • VER = protocol version (this specification defines version 3, i.e., both bits set)
  • PKT TYPE = packet type
  • S = full 32-byte source address included (when clear, a compact source hint is used instead; see Source Address for hint size by packet type)
  • R = reserved; senders MUST set to 0; receivers MUST drop packets where this bit is non-zero
  • H = flood hop count present. Zero if direct. Cleared before the byte enters the AAD

Packet Type Values

ValueName
0BCST: Broadcast
1UACK: MAC Ack
2UNIC: Unicast
3UNAR: Unicast, Ack-Requested
4MCST: Multicast
5RESERVED
6BUNI: Blind Unicast
7BUAR: Blind Unicast, Ack-Requested

Common Optional Fields

Flood Hop Count

If present, FHOPS is a single byte containing two 4-bit fields:

  7   6   5   4   3   2   1   0
+---------------+---------------+
| FHOPS_REM     | FHOPS_ACC     |
+---------------+---------------+
     4 bits          4 bits

Where:

  • FHOPS_REM (high nibble) = hops remaining—the number of additional flood hops remaining. Decremented by each flood-forwarding repeater. When zero, no further flood forwarding is allowed.
  • FHOPS_ACC (low nibble) = hops accumulated—the number of flood hops already traversed. Incremented by each flood-forwarding repeater.

Important

FHOPS counts flood hops only. A repeater that forwards a packet because it matched a source-route hint MUST leave the byte untouched—including the repeater that removes the last remaining hint. Emptying the route makes the packet floodable, but the first flood hop is performed by the next repeater, which sees an empty route and pays for it there.

The sum FHOPS_REM + FHOPS_ACC is constant across forwarding hops and usually equals the original flood hop limit set by the sender. An exception to this rule is bridging, which can decrease FHOPS_REM unilaterally. The maximum flood radius is 15 flood hops, which is a path of sixteen hops when no source route is involved; longer paths can be achieved by combining source routing with flooding (see Routing Implications).

FHOPS_ACC enables the destination to determine how many flood hops the packet traversed—one less than the hops it took, unless a source route carried it part of the way—which is used for MAC ack routing when no complete trace is available.

Options Field

Options use the same delta-length encoding as CoAP (RFC 7252 §3.1). Each option is encoded as a delta from the previous option’s number, a length, and a value. The sequence is terminated by a 0xFF byte if a payload is present.

Option Encoding

Each option begins with a single byte containing two 4-bit fields:

  7   6   5   4   3   2   1   0
+---------------+---------------+
| Option Delta  | Option Length |
+---------------+---------------+
     4 bits          4 bits

Followed by optional extended delta bytes, optional extended length bytes, and then the option value:

+---------------+---------------+
| Option Delta  | Option Length |  (1 byte)
+---------------+---------------+
| Extended Delta (0-2 bytes)    |
+-------------------------------+
| Extended Length (0-2 bytes)   |
+-------------------------------+
| Option Value (0 or more bytes)|
+-------------------------------+

Delta and length interpretation:

Nibble valueMeaning
0–12Literal value
13One extended byte follows; value = byte + 13
14Two extended bytes follow; value = uint16 (big-endian) + 269
15Reserved—used only in the delta field to indicate the 0xFF end-of-options marker

The value 15 is legal only as part of the 0xFF end-of-options marker, where both nibbles are 15. Any other appearance of nibble value 15—a delta nibble of 15 whose length nibble is not 15, or a length nibble of 15 in an ordinary option record—is malformed, and the packet MUST be dropped.

The option delta is the difference between this option’s number and the previous option’s number (or zero for the first option). Options must appear in order of increasing option number. Multiple options with the same number are permitted (delta = 0).

Option numbers range from 0 to 0xFFFF. Deltas accumulate, so a sequence of records can name a larger number; such a record is malformed, and the packet MUST be dropped.

End-of-Options Marker

The byte 0xFF (delta nibble = 15, length nibble = 15) separates the options block from a following variable-length payload.

Parsing proceeds as follows:

  1. Determine the options+payload region: buf[options_start .. packet_end − trailer_len], where trailer_len is the length of the fixed-size trailer at the end of the packet (the MIC length from SECINFO, plus 64 when the SCF’s D bit is set; 8 for the MAC ack trailer; or 0 for broadcast). The trailer length is known before options are parsed. With encryption enabled, the 64 bytes before the MIC are the encrypted SIG.
  2. Scan options in order through that region, consuming each delta-length-value record. Because option records are variable-length, this scan is the only way to locate the end of the options block.
  3. If a 0xFF byte is encountered during the scan, the bytes remaining in the region (between the 0xFF and the trailer) are the payload. A 0xFF with zero bytes remaining is a valid empty payload.
  4. If the region is exhausted without encountering a 0xFF, there is no payload.

Senders SHOULD omit 0xFF when there is no payload. Receivers MUST accept a 0xFF in any position where the marker is syntactically valid (i.e., immediately after the last option record), regardless of if payload bytes follow.

Example

Two options—option 3 (1-byte value) followed by option 9 (2-byte value):

+------+-------+  +------+-------+-------+  +------+
| 0x31 |  val  |  | 0x62 |  val  |  val  |  | 0xFF |
+------+-------+  +------+-------+-------+  +------+
 delta=3 opt 3     delta=6 opt 9             marker
 len=1   val (1B)  len=2   val (2B)

Frame Types

Broadcast Packet

Broadcast packets carry a source and payload, with no SECINFO and no MIC. A broadcast may carry a Frame Signature option, which supplies a frame counter and an Ed25519 signature that attributes the frame to its source key.

+-----+-------+-----+---------+---------+
| FCF |[FHOPS]| SRC | OPTIONS |[PAYLOAD]|
+-----+-------+-----+---------+---------+
  1 B   0/1 B  3/32B  variable    var.

A broadcast with an empty payload is a Beacon. Beacons omit the 0xFF end-of-options marker since no payload follows (see End-of-Options Marker).

MAC Ack Packet

A MAC acknowledgement is generated by the final destination—the node that successfully processes and accepts the original packet. Repeaters do not generate MAC acks; they forward ack packets like any other packet type.

If the original packet is received again because its acknowledgement was lost, the final destination MAY retransmit the same acknowledgement without accepting or delivering the original packet again, subject to the bounded duplicate-acknowledgement window.

The ack carries two fixed trailer fields: an ack MIC—the first four bytes of the original packet’s MIC, which lets the original sender (and any repeater that forwarded the original) correlate the ack with the packet it acknowledges—and an ack tag—a MIC over the ack’s static options, bound to the packet it acknowledges, that only the original sender can verify. The ack carries no destination hint. See Ack Tag Construction for the derivation.

+-----+-------+---------+---------+---------+
| FCF |[FHOPS]| OPTIONS | ACK MIC | ACK TAG |
+-----+-------+---------+---------+---------+
  1 B   0/1 B  variable     4 B       4 B

Where:

  • ACK MIC is the first 4 bytes of the original packet’s on-wire MIC—a publicly derivable correlation handle, not an authenticator
  • ACK TAG is a 4-byte MIC computed with the pairwise K_mic over the ack’s FCF and static options, bound to the original packet’s full S2V output (see Ack Tag Construction)

The ACK MIC is public—any observer who received the original packet can compute it—so it provides correlation, not authentication. Authentication comes from the ACK TAG, which requires knowledge of the pairwise K_mic and cannot be produced by an observer who lacks it, even one who received the original packet in its entirety. The tag covers the ack’s static options, such as an operator callsign, so those cannot be altered in transit; dynamic options and the flood hop count remain open to repeaters, as on any packet. Because the ack carries no destination hint and the correlation handle is only a prefix of the (already public) original MIC, the ack adds no explicit endpoint identifier: an observer who received the original packet learns that it was delivered, but the ack itself names neither party. This removes the direct sender-identity leak a destination hint would introduce; it does not by itself defeat correlation by other means (timing, RF fingerprinting, or tying the ack back to an original packet that already exposed endpoint hints). For blind unicast, whose forward frame conceals both endpoints from non-channel observers, this keeps the ack from undoing that concealment.

Because the ack carries no destination hint, it is a return-routed token: it is routed back to the original sender using whatever routing state is available—a cached source route, a cached flood response scoped by FHOPS_ACC and any learned region-code options, or both. See Route Learning for how nodes learn and cache routing information from incoming packets. For reliable ack delivery over long source-routed paths, the original sender should include a Trace option. The original sender matches an incoming ack to an outstanding request by its ACK MIC, then verifies the ACK TAG; a colliding ACK MIC from an unrelated exchange fails tag verification and is discarded.

Because the ack trailer (ACK MIC followed by ACK TAG) is a known fixed length and no payload follows, the options field MUST be followed immediately by the trailer with no intervening bytes. The 0xFF end-of-options marker MAY be omitted (the trailer length is what bounds the options region). If the marker is present it MUST be the last byte of the options region—receivers MUST reject packets that carry trailing bytes between the marker and the ack trailer.

Unicast Packet

Unicast packets are addressed by destination hint and carry the source address.

+-----+-------+-----+-----+---------+---------+---------+-------+------+
| FCF |[FHOPS]| DST | SRC | SECINFO | OPTIONS |[PAYLOAD]| [SIG] | MIC  |
+-----+-------+-----+-----+---------+---------+---------+-------+------+
  1 B  0/1 B   3 B  3/32B   5/7 B    variable    var.    0/64 B 4-16 B

DST is the first three bytes of the recipient’s public key. SIG is present when the SCF’s D bit is set (see Frame Signatures); this holds for every packet type that carries SECINFO.

Receivers first use DST as a cheap filter, then use the source public key (or its cached equivalent when only a hint is present) and their own key to derive the shared secret and authenticate/decrypt the packet.

Unicast Packet with Ack Requested

This is identical to unicast, but the packet-type value signals that a MAC acknowledgement is requested.

+-----+-------+-----+------+---------+---------+---------+-------+------+
| FCF |[FHOPS]| DST | SRC  | SECINFO | OPTIONS |[PAYLOAD]| [SIG] | MIC  |
+-----+-------+-----+------+---------+---------+---------+-------+------+
  1 B  0/1 B   3 B   3/32B   5/7 B    variable    var.    0/64 B 4-16 B

Semantics differ, wire layout does not.

Multicast Packet

Multicast packets carry a 2-byte channel identifier derived from the channel key.

Channel Identifier Derivation

channel_identifier = HKDF-SHA256(channel_key, salt="UMSH-CHAN-ID", info="", L=16)
channel_id         = first_2_bytes( channel_identifier )

The CHANNEL field carries channel_id. The full 16-byte channel identifier is what names a channel where two channels must be told apart without revealing either key—two keys can derive the same 2-byte channel_id, and management interfaces cannot leave that ambiguous. The same derivation extended to 32 bytes is the channel commitment that binds a frame signature to its channel.

Encrypted Multicast (E = 1)

When encryption is enabled, the source address is encrypted together with the payload, concealing the sender’s identity from observers who do not possess the channel key.

+-----+-------+---------+---------+---------+----------------------------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | ENCRYPT(SRC+PAYLOAD+[SIG]) | MIC  |
+-----+-------+---------+---------+---------+----------------------------+------+
  1 B  0/1 B     2 B     5/7 B    variable       3/32 + var. + 0/64     4-16 B

The SRC inside the ciphertext follows the S flag convention: a 3-byte hint when S is clear, or the full 32-byte public key when S is set.

Only a node with the correct channel key can recover the source address and payload.

Unencrypted Multicast (E = 0)

When encryption is not enabled, the source address appears in cleartext, but in the same place that it appeared in encrypted multicast. SRC is covered by the MIC as part of the plaintext, just as when it is encrypted (see Associated Data):

+-----+-------+---------+---------+---------+------+---------+-------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | SRC  | PAYLOAD | [SIG] | MIC  |
+-----+-------+---------+---------+---------+------+---------+-------+------+
  1 B  0/1 B     2 B     5/7 B    variable  3/32 B    var.    0/64 B 4-16 B

Blind Unicast Packet

Blind unicast uses a multicast channel to conceal sender and destination metadata from observers without the channel key while still protecting the payload end-to-end for the actual destination.

Like other channel-addressed packets, blind unicast honors the E flag in SECINFO.

Encrypted Blind Unicast (E = 1)

+-----+-------+---------+---------+---------+-------------+-------------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | ENC_DST_SRC | ENC_PAYLOAD | MIC  |
+-----+-------+---------+---------+---------+-------------+-------------+------+
  1 B  0/1 B     2 B     5/7 B    variable      6/35 B    var. + 0/64   4-16 B

The MIC is computed using the blind unicast payload keys, which combine the pairwise shared secret with the channel key. It covers the payload, and the plaintext DST and SRC as part of the associated data. ENC_DST_SRC is encrypted using the channel’s derived encryption key K_enc_channel (see Multicast Packet Keys) and an IV derived from the MIC (see Blind Unicast Address Encryption). Any change to ENC_DST_SRC changes the recovered destination or source address, and the MIC check rejects the packet.

Unencrypted Blind Unicast (E = 0)

When encryption is disabled, blind unicast still uses the channel identifier and the blind-unicast packet type, but the destination hint, source address, and payload appear in cleartext:

+-----+-------+---------+---------+---------+-----+------+---------+-------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | DST | SRC  | PAYLOAD | [SIG] | MIC  |
+-----+-------+---------+---------+---------+-----+------+---------+-------+------+
  1 B  0/1 B     2 B     5/7 B    variable   3 B  3/32 B    var.    0/64 B 4-16 B

In this mode, the packet remains channel-associated and authenticated with the blind-unicast keys, but it does not conceal sender or destination metadata. This can still be useful when an implementation wants channel-associated unicast semantics without encryption.

Blind Unicast Processing

  1. Receiver uses CHANNEL to identify candidate channel keys.
  2. Receiver derives the channel’s candidate keys via HKDF.
  3. If E = 1, receiver reads the MIC and uses K_enc_channel plus MIC to decrypt ENC_DST_SRC, recovering the destination hint and sender address.
  4. If E = 0, receiver reads DST and SRC directly from the cleartext packet.
  5. Receiver converts the sender Ed25519 public key into an X25519 public key.
  6. Receiver converts its own Ed25519 private key into an X25519 private key.
  7. Receiver performs ECDH and derives the stable pairwise keys.
  8. Receiver computes the blind unicast payload keys by XORing the pairwise keys with the channel keys.
  9. If E = 1, receiver decrypts ENC_PAYLOAD using the blind unicast payload keys.
  10. Receiver verifies the blind-unicast MIC, whose associated data includes the plaintext DST and SRC whether or not E is set.
  11. If verification fails, the packet is rejected. This covers a tampered address block as well as a tampered payload.

Some repeaters may decline to forward blind unicast packets for unknown channels.

Blind Unicast with Ack Requested

Same wire layout as blind unicast, but with ack-requested semantics.

+-----+-------+---------+---------+---------+-------------+-------------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | ENC_DST_SRC | ENC_PAYLOAD | MIC  |
+-----+-------+---------+---------+---------+-------------+-------------+------+
  1 B  0/1 B      2 B     5/7 B    variable     6/35 B    var. + 0/64   4-16 B

Packet Options

UMSH packet options use the delta-length encoding described in Packet Structure. Each option has a numeric option number whose low four bits encode its semantic attributes:

  • Bit 0: Forwarding-Critical (1) / not (0)
  • Bit 1: Dynamic (1) / Static (0)
  • Bits 2 and 3: both set on a static option ((number & 0xE) == 0xC): Delivery-Critical

This means a node can determine an unrecognized option’s attributes by inspecting its option number without consulting a registry.

Attribute Encoding

The attribute combinations and their option number patterns, by the low four bits of the number:

Low 4 bitsOption numbersClassification
0, 4, 80, 4, 8, 16, 20, 24, …Non-Critical, Static
1, 5, 91, 5, 9, 17, 21, 25, …Forwarding-Critical, Static
2, 6, 10, 142, 6, 10, 14, 18, …Non-Critical, Dynamic
3, 7, 11, 153, 7, 11, 15, 19, …Forwarding-Critical, Dynamic
1212, 28, 44, …Delivery-Critical, Static
1313, 29, 45, …Forwarding-Critical, Delivery-Critical, Static

Dynamic options are never delivery-critical. Delivery-criticality is an end-to-end demand from the sender, and only a static option, which the MIC and any frame signature cover, can carry one.

Forwarding and Delivery Criticality

These determine behavior when a node encounters an unknown option. The two classes are independent, and a node that both forwards and receives a packet applies each check to its own role.

  • Forwarding-Critical (bit 0 set): if unrecognized, a repeater does not forward the packet. It can still deliver the packet locally.
  • Delivery-Critical (low four bits 12 or 13): if unrecognized, an endpoint does not accept, deliver, or acknowledge the packet. It drops the packet and counts the drop. Repeaters forward it as normal.
  • Non-Critical: if unrecognized, the option is ignored and the node continues processing.

A recognized option whose value the node cannot interpret, such as one of the wrong length, is handled as unrecognized, within the option’s own classes.

MAC acks are exempt from delivery-critical interpretation. An ack delivers nothing, so a receiver accepts an otherwise valid ack whatever delivery-critical options it carries. Every static option on an ack, recognized or not, is still covered by its ack tag.

Dynamic vs. Static

These determine whether an option is covered by the MIC:

  • Dynamic (bit 1 set): not protected by the security MIC; may be modified in transit by repeaters
  • Static (bit 1 clear): protected by the security MIC; must not be modified in transit

This distinction allows forwarding-related metadata (source routes, traces, station callsigns) to be modified by repeaters without invalidating end-to-end authentication.

Defined Options

NumberNameClassificationValue
0Frame SignatureNon-Critical, Static68 bytes
1UNASSIGNEDForwarding-Critical, Static
2UNASSIGNEDNon-Critical, Dynamic
3Source RouteForwarding-Critical, Dynamic0+ bytes
4Operator CallsignNon-Critical, StaticARNCE/HAM-64
5Minimum RSSIForwarding-Critical, Static0–1 bytes
6Route RetryNon-Critical, Dynamic0 bytes
7TraceForwarding-Critical, Dynamic0+ bytes
8Ack MICNon-Critical, Static4 bytes
9Minimum SNRForwarding-Critical, Static0–1 bytes
10UNASSIGNEDNon-Critical, Dynamic
11Region CodeForwarding-Critical, Dynamic2 bytes
15Station CallsignForwarding-Critical, DynamicARNCE/HAM-64
257FenceForwarding-Critical, Static0 bytes

Frame Signature (option 0)

  • Type: FRAME COUNTER (4 B, big-endian) ‖ SIG (64 B), exactly 68 bytes.
  • Semantics: a frame signature for a broadcast, which has no SECINFO to carry a counter or the D bit. SIG is an Ed25519 signature by the source key over the signature input, in which this option’s 64 signature bytes are replaced by zeros. FRAME COUNTER is drawn from the sender’s one counter sequence, and a verified frame is judged by the signed-frame counter rules.
  • Defined for broadcast only. A sender MUST NOT include it on a packet type that carries SECINFO, which signs with the SCF’s D bit instead; a receiver ignores it there.
  • A value of any other length is handled as an unrecognized option.
  • A sender MUST NOT include more than one. A broadcast carrying more than one is treated as unsigned.
  • The full source key is not required. A hint-only signed broadcast can be verified only by a receiver that already holds the sender’s key.

Source Route (option 3)

  • Semantics: contains an ordered list of repeater hints designating the forwarding path.
  • A sender MUST NOT include more than one. A packet carrying more than one is handled as carrying an unrecognized forwarding-critical option.
  • Repeater behavior:
    • Only the repeater matching the first hint may forward the packet.
    • That repeater removes its own hint before retransmission.
    • If removing its own hint leaves zero remaining hints, the repeater still preserves the source-route option with an empty value.
      • This is important: the forwarded packet still carries the information that it was explicitly source-routed, even though the route is now exhausted.
      • Removing the last hint does not change what kind of hop this is. The repeater was named, so it forwards as a source-routed hop and leaves FHOPS, region policy, and signal-quality thresholds alone. The next repeater sees the empty option and is the first to flood.
    • Repeaters that do not match the first hint must not forward the packet.
  • Value layout: see Source Route Option Value.

Operator Callsign (option 4)

  • Encoding: ARNCE/HAM-64 (2, 4, 6, or 8 bytes; encodes callsigns up to 12 characters)
  • Semantics: identifies the original packet sender’s amateur radio callsign.
  • Use: required for locally originated packets in Licensed-Only amateur operation.
  • In Hybrid operation, its presence marks the packet as eligible for forwarding under amateur-radio authority; packets without it may still be forwarded under unlicensed authority if local rules allow.

Minimum RSSI (option 5)

  • Type: unsigned 1-byte integer, interpreted as a negative dBm value
  • A sender MUST NOT include more than one. A packet carrying more than one is handled as carrying an unrecognized forwarding-critical option.
  • Semantics: packet must be received with at least this RSSI to be flood-forwarded. This option does not apply to source-routed hops.
  • Example: value 130 means -130 dBm
  • If present with no value (length 0), default is -100 dBm (THIS VALUE IS SUBJECT TO CHANGE)
  • If a repeater has a locally configured minimum RSSI, it must use the higher of the packet’s minimum RSSI threshold and the repeater’s configured minimum RSSI threshold.

Route Retry (option 6)

  • Type: flag. A sender includes it with no value; a receiver treats the option as present whatever its value, and a forwarder carries the value unchanged.

  • Semantics: indicates that the originator is re-attempting delivery of the same logical packet after its acknowledgment timed out: either a route it had assumed failed, or, for a packet that assumed none, the acknowledgment was lost on its way back (Route Failure Recovery).

  • A sender MUST NOT include more than one. A receiver reads the first and ignores the rest, and a forwarder carries only the first.

  • This option is intended for sender-originated route recovery, not for ordinary first transmission. A sender sends at most one Route Retry of a logical packet.

  • The failed assumption need not be a source route. A sender that narrowed FHOPS because it believed the destination was directly reachable, or reachable within a known flood distance, has made the same kind of assumption; nothing on the wire distinguishes that packet from an ordinary short-radius flood, and the recovery is the same.

  • When present, repeaters treat the packet as a distinct forwarding attempt for duplicate-suppression purposes even though the MIC and frame counter are unchanged.

  • The destination does not treat this option as creating a new logical packet. Replay acceptance and duplicate application delivery remain governed by the packet’s normal security state, especially its frame counter.

  • A Route Retry that duplicates a packet the destination already acknowledged tells it that acknowledgment was lost. The destination re-acknowledges by a scoped, traced flood instead of along the route the lost acknowledgment took (Lost Acknowledgments).

  • A sender using this option for route recovery typically:

    • removes the stale source-route option, if one was present
    • adds or refreshes flood hops, up to but not beyond the budget the sending application was willing to spend; a packet that already flooded at that budget keeps it
    • includes a Trace option to learn a replacement route
    • preserves the same frame counter and payload

    Each of these rewrites a field the associated data excludes, which is what lets the retry reuse the original MIC unchanged. Adding FHOPS to a packet that had none also sets the FCF’s H bit, and the AAD clears that bit for this reason.

Trace (option 7)

  • Semantics: if present, each hop that forwards the packet prepends a record of itself before retransmitting, so the value lists the hops the packet took, most recent first. If absent, no trace information is added.
  • Each record names the hop’s router hint and the signal with which that hop received the packet. Value layout and forms: see Trace Option Value.
  • The option is forwarding-critical. A repeater that does not implement it does not forward a traced packet, so a trace never silently skips a hop; that repeater still forwards untraced packets.
  • A sender MUST NOT include more than one. A receiver reads the first and ignores the rest, and a forwarder carries only the first.
  • A malformed value is handled as unrecognized: a repeater does not forward the packet, and a receiver ignores the trace and still delivers the packet.

Signal. A record’s RSSI byte is the negative of the RSSI in dBm (so −90 dBm becomes 90), and its SNR byte is the signed SNR in cB (centibels, or tenths of a dB). A hop that received the packet over a point-to-point link rather than the air—a bridge, for example—writes zero for both. The zero entry marks a hop with no radio in it, so it has no signal to report and does not lose packets to fading. A hop that received the packet over the air never writes it: a reading of 0 dBm or stronger is carried as an RSSI byte of 1.

Sender. A sender that includes a Trace option originates the packet at least one byte below the frame limit, which leaves room for the last-resort form on a link with the same limit when the repeater adds nothing else. A repeater that also inserts a Region Code or inserts or lengthens a Station Callsign may need more than that byte; if even the last-resort form does not fit, the repeater does not forward the packet on that link.

Receiver.

  • A complete trace, with signal or hints only, is a complete path. It is used for routes and hop counts; a hints-only trace lacks only the per-hop signal, and the receiver still measures the final link’s signal itself.
  • A truncated trace reports the most recent hops but is not a complete path. Nothing builds a route from it: route learning, acknowledgement return routes, and source routes built from observed hints all require a complete trace.
  • The hop count of a packet with a truncated trace is FHOPS_ACC + 1 if the packet carries no Source Route option, and unknown if it carries one.

Ack MIC (option 8)

This option is a piggy-backed acknowledgement: a packet carrying it acknowledges the packet whose ack_mic it names, exactly as a MAC ack for that packet would. A destination that answers an ack-requested packet with a unicast or blind unicast of its own MAY include the option in the answer, whether or not it also sends the standalone MAC ack. The original sender takes whichever of the two arrives first as the acknowledgement; the other then matches no outstanding packet and is discarded.

  • Type: 4-byte ack MIC—the first 4 bytes of the acknowledged packet’s on-wire MIC (see Ack Tag Construction).

The option carries only the correlation handle, not a keyed ack tag: the packet carrying the option is itself authenticated to the original sender, so its own MIC already proves the acknowledgement is genuine. The ack_mic value simply identifies which outstanding request the reply acknowledges. The original sender accepts the option only on a packet authenticated as coming from the destination of the packet it names.

The option is a courtesy to the original sender. A sender that cannot fit it into a frame omits it.

Because the option sits in the plaintext options block, forwarders read it under the same terms as a standalone MAC ack, including for ack cancellation.

On a Verify Frame the option names the packet to be verified instead of acknowledging it: the sender holds that packet but has not accepted it, so the original sender MUST NOT treat the option as an acknowledgement (see Counter Resynchronization). Forwarders still apply ack cancellation, since the named packet has reached its destination.

Minimum SNR (option 9)

  • Type: signed 1-byte integer, in dB
  • Semantics: packet must be received with at least this SNR to be flood-forwarded. This option does not apply to source-routed hops.
  • If present with no value (length 0), default is -3 dB. (THIS VALUE IS SUBJECT TO CHANGE)
  • A sender MUST NOT include more than one. A packet carrying more than one is handled as carrying an unrecognized forwarding-critical option.
  • If a repeater has a locally configured minimum SNR, it must use the higher of the packet’s minimum SNR and the repeater’s configured minimum SNR.

Region Code (option 11)

  • Type: 2-byte region identifier
  • Semantics: restricts flood-routing to repeaters configured for the specified region.
  • A repeater configured for one or more regions MUST NOT flood-forward a packet whose region is none of them. A repeater configured for no regions makes no regional claim and applies no such restriction.
  • This option MUST NOT be enforced on a source-routed hop, which includes the hop that removes the last remaining hint. A repeater named in the route forwards regardless of region.
  • Multiple region-code options may appear on the same packet. In that case, a repeater MAY flood-forward the packet if any one of the listed regions matches local policy.
  • Because this option is dynamic, repeaters may insert it while flood-forwarding a packet that currently has no region code.
  • A repeater must never rewrite an existing region code and must never add a second region code to a packet that already has one or more region-code options.
  • Region insertion is a local policy decision. When no explicit local policy exists, a reasonable default is the IATA code of the closest regional commercial airport.
  • Region insertion applies only during flood forwarding. An untagged source-routed packet is first tagged by the repeater that flood-forwards it—the one that receives it with an already-empty source route—not by the repeater that emptied the route.

Region Code Encoding

Region codes are 2-byte identifiers derived by one of two methods, depending on the type of region. Both ignore ASCII case—ARNCE/HAM-16 encodes A–Z and a–z alike, and the hash folds A–Z to lowercase a–z before it is taken, altering no other character. Strings that differ only in ASCII case therefore derive the same code and name the same region.

As with channel names, folding is restricted to ASCII: correct case-folding of the full Unicode range is locale-dependent, and a name that folded under one implementation and not another would be two regions on one mesh.

Short codes. A region named by one to three ASCII letters or digits encodes directly into a 16-bit value with ARNCE/HAM-16, the unused trailing characters left as NUL. Three letters are conventionally the IATA code of the nearest airport or of a metro area that has one; two are conventionally an ISO 3166-1 alpha-2 country code or a bare subdivision code, for a region that spans one. Examples:

Short CodeRegion Code
SJC0x7853
MFR0x5242
US0x8638
WA0x8FE8

An all-letter short code is exclusive: the transform below vacates every one of them, so no named region can ever derive one. A short code bearing a digit—W7, 5X2—encodes just as faithfully and no two short codes ever share a code, but it is not vacated, so it shares its space with the hashes. Such a code is worth using when a mnemonic is wanted in place of a hash, and it is nothing more than that: an implementation MUST NOT display a region code as a digit-bearing short code, because the code it is reading may equally have come from a name.

/, -, and ^, though ARNCE spells them, MUST NOT appear in a short code. The transformed codes are led by / and -, and a short code able to reach that space would read back as a region it is not.

Named regions. For regions that a short code does not suit (super-regions, cities without a nearby airport, geographic areas, etc.), the region code is the first two bytes of the SHA-256 hash of the folded region name (UTF-8 encoded), EXCEPT when performing ARNCE/HAM-16 decoding on the resulting value would yield nothing but letters—one, two, or three of them. In that case, you additionally perform the following transform:

def transform_letter_chunk(encoded: int) -> int:
    """Transform an all-letter ARNCE chunk into a non-letter ARNCE chunk."""

    LETTER_MIN = 1
    LETTER_MAX = 26

    TRANSFORM_BASE = 27 * 1600                    # 0xA8C0
    TWO_LETTER_BASE = TRANSFORM_BASE + 26 ** 3    # 0xED68
    ONE_LETTER_BASE = TWO_LETTER_BASE + 26 ** 2   # 0xF00C

    a = encoded // 1600
    b = (encoded // 40) % 40
    c = encoded % 40

    def is_letter(x: int) -> bool:
        return LETTER_MIN <= x <= LETTER_MAX

    if not is_letter(a):
        return encoded

    if is_letter(b) and is_letter(c):
        return TRANSFORM_BASE + (a - 1) * 26 * 26 + (b - 1) * 26 + (c - 1)
    if is_letter(b) and c == 0:
        return TWO_LETTER_BASE + (a - 1) * 26 + (b - 1)
    if b == 0 and c == 0:
        return ONE_LETTER_BASE + (a - 1)
    return encoded

The three lengths occupy three consecutive blocks, longest first, and together vacate 18,278 of the 64,000 encodable chunks. The highest code the transform yields is 0xF025; every one of them decodes to a string led by / or -, and so to nothing that reads as a region.

Examples, with the folded name each hash is taken over:

Region NameFoldedSHA-256 prefixRegion Code
Willamette Valleywillamette valley0xb02d...0xB02D
East Bayeast bay0x36e2...0x36E2
Rogue Valleyrogue valley0x3f56...0xC0F9
Wasatch Frontwasatch front0x5fa0...0xEEDF

Note that the first two bytes of the SHA256 of “rogue valley” is 0x3F56, which would decode to JEN, so it is transformed to 0xC0F9, which decodes to no letters. “wasatch front” hashes to 0x5FA0, which would decode to the two letters OL, and is transformed to 0xEEDF for the same reason.

Thus, a hash-derived region code will never collide with an all-letter short code. This allows all region codes which decode to nothing but letters to be assumed to be short codes and to be used/displayed unambiguously without additional context.

However, collisions can still happen between hash-originated region codes, and between one of those and a digit-bearing short code. These collisions are rarely of practical concern. If a region code in one part of the world collides with a region code in a different part of the world, there is no actual ambiguity because flood repeating is an inherently local event. In the rare case of a collision within a geographic area, it can be resolved by adjusting the named region slightly (for example, making it more specific).

The assignment and scope of hash-derived region codes—and resolution of any collisions—are generally handled locally.

Literal codes. In any context that takes a region’s string form, a string of 0x followed by four hexadecimal digits (e.g. 0x31d9) denotes the two-byte code it spells. Strings of this shape are by definition excluded from the other two derivations—there is no named region 0x31d9, only the code. This is also the display form for a code that reads as no short code and whose name is unknown, so a region list read off the air can be fed back in unchanged. Note that a string of one to three characters is a short code before it is anything else: 0x1 encodes as the three characters 0, x, 1, and only 0x followed by four hexadecimal digits is a literal.

A region name MUST NOT exceed 24 bytes of UTF-8. Names travel on the wire in identity payloads, and the bound is what lets a list of them fit there.

Capitalization. Folding applies to derivation and comparison, not to storage: wherever a region is held or carried in its string form—a repeater’s configured list, the Supported Regions identity option—it keeps the capitalization it was written with. A set of regions holds at most one entry per folded string; writing a region already present in a different capitalization respells the entry rather than adding a second one. A short code is displayed uppercase whatever case it was written in.

Station Callsign (option 15)

  • Encoding: ARNCE/HAM-64 (2, 4, 6, or 8 bytes; encodes callsigns up to 12 characters)
  • Semantics: identifies the transmitting station’s amateur radio callsign.
  • If absent, the station callsign is assumed to equal the source callsign (if present)
  • This option is forwarding-critical because repeaters must replace or remove it during forwarding.
  • Use:
    • in Licensed-Only mode, repeaters replace or insert it on every forwarded packet
    • in Hybrid mode, repeaters also replace or insert it on every forwarded packet
    • in Unlicensed mode, repeaters remove it if present and do not add their own

Fence (option 257)

  • Type: flag. A receiver ignores it whatever its value, and a forwarder carries it unchanged.
  • Semantics: none. A sender does not include it.

Routing Option Layouts

Source Route Option Value

A source-route option contains zero or more router hints:

+----------+----------+----------+-----+
|  RH[0]   |  RH[1]   |  RH[2]   | ... |
+----------+----------+----------+-----+
    2 B         2 B       2 B

Where each RH[i] is the first two bytes of a repeater’s public key.

Interpretation:

  • RH[0] is the next repeater that must forward the packet
  • when that repeater forwards, it removes RH[0]

An empty source-route option indicates that all explicit routing hints have been consumed.

  • For forwarding purposes, an empty source-route option behaves the same as an absent source-route option: there is no remaining explicit next hop.
  • However, it is still semantically useful and should be preserved when produced by forwarding, because it records that the packet did in fact traverse an explicit source-routed path before the hints were exhausted.

Trace Option Value

A complete trace with signal contains zero or more 4-byte records:

+----------+--------+--------+----------+--------+--------+-----+
|  RH[0]   | RSSI[0]| SNR[0] |  RH[1]   | RSSI[1]| SNR[1] | ... |
+----------+--------+--------+----------+--------+--------+-----+
    2 B       1 B      1 B       2 B       1 B      1 B

Each hop prepends its own record:

new_trace = my_router_hint || rssi || snr || old_trace

So the list is ordered most-recent hop first. A bridge prepends a record as a repeater does, with zero signal bytes.

A trace can outgrow the frame limit as hops add records: each record adds 4 bytes, and the option’s length field gains a byte once the value passes 12 bytes. The path matters more than the per-hop signal, so a trace that no longer fits first sheds its signal and only then drops hops. The value takes one of three forms:

FormFirst byteLengthContents
Complete, with signalanymultiple of 44-byte records
Complete, hints only0xFEodd0xFE ‖ 2-byte router hints
Truncated, hints only0xFFodd0xFF ‖ 2-byte router hints, oldest dropped

Hints are most recent first in every form, and a lone 0xFE or 0xFF is valid. The length tells the hints-only forms apart from a record list whose first router hint happens to begin with 0xFE or 0xFF. Every other value is malformed: a length of 2 modulo 4, or an odd length beginning with any other byte.

Before forwarding a traced packet, a repeater computes the exact size of the forwarded packet on the outgoing link, including any options it inserts, and takes the first of these that fits:

  1. Add its own entry in the trace’s current form: a 4-byte record to a trace with signal, or its 2-byte hint to a 0xFE or 0xFF trace.
  2. For a trace with signal: convert it to 0xFE ‖ hints and add its own hint.
  3. Convert it to 0xFF ‖ hints, add its own hint, and drop hints from the end, oldest first, until the packet fits. A lone 0xFF is the last resort.
  4. If even a lone 0xFF does not fit, do not forward the packet on that link, and count the drop.

Forms only degrade. No repeater adds signal to a hints-only trace, so the forms never mix, and a 0xFF trace never returns to 0xFE.

Security & Cryptography

UMSH authenticates and optionally encrypts packets using AES-SIV (RFC 5297) with AES-256. Unicast packets are secured with pairwise keys derived from X25519 ECDH between sender and recipient Ed25519 keys. Multicast packets are secured with keys derived from the shared channel key. In both cases, a monotonic frame counter provides replay protection without requiring synchronized clocks. Any frame other than a MAC ack may also carry an Ed25519 frame signature that attributes it to its sender’s key.

Each secured packet carries a Security Information (SECINFO) field containing a Security Control Field, a frame counter, and an optional salt. The sections below describe these fields, the key derivation process, and the cryptographic operations applied to each packet.

Security Information (SECINFO)

SECINFO Encoding

+--------+--------------------+----------------+
|  SCF   | FRAME COUNTER (4B) | [SALT (2B)]    |
+--------+--------------------+----------------+
   1 B            4 B              0/2 B

Security Control Field

  7   6   5   4   3   2   1   0
+---+-------+---+---+-----------+
| E |  MIC  | S | D | RESERVED  |
+---+-------+---+---+-----------+
 1b   2 bits 1b  1b    3 bits

Where:

  • E = encrypted payload flag
  • MIC = MIC size code
  • S = salt included
  • D = signed: a 64-byte frame signature follows the payload
  • RESERVED = must all be set to zero

If the RESERVED bits read as anything other than zero, the packet MUST be dropped by the recipient.

MIC size encodings:

ValueMIC Length
04 bytes
18 bytes
212 bytes
316 bytes

The MIC is produced by computing the full 16-byte synthetic IV V with S2V (RFC 5297 §2.4) and truncating to the specified length. The S2V output is an AES-CMAC, and truncation of CMAC outputs is permitted by NIST SP 800-38B.

MIC Size Selection Guidance

Shorter MICs save bytes on the wire but reduce forgery resistance and increase the probability of duplicate-suppression collisions in repeater caches (see Duplicate Suppression). The following guidelines help choose an appropriate MIC size:

  • 16 bytes (default): Recommended for long-term stable identities where the same pairwise keys may be used for months or years. The cost of a successful forgery is high (attacker gains persistent access to impersonate a node), and the 2^-128 forgery probability makes brute-force infeasible regardless of how many packets an attacker can attempt.

  • 8 bytes: A reasonable middle ground for most communication. Provides 2^-64 forgery probability—well beyond practical brute-force for LoRa’s low packet rates—while saving 8 bytes per packet. Suitable for general unicast and multicast traffic.

  • 4 bytes: Appropriate for short-lived contexts where the keys will be discarded soon, such as PFS sessions or one-time exchanges using ephemeral node addresses. The 2^-32 forgery probability (~1 in 4 billion) is adequate when the window of exposure is brief. Also useful for latency-sensitive or payload-constrained scenarios where every byte matters, such as sensor telemetry on slow LoRa links.

  • 12 bytes: Available as an intermediate option when 8 bytes feels too tight but 16 bytes is more overhead than warranted. Provides 2^-96 forgery probability.

As a general principle: the longer the keys will be in use and the higher the value of the traffic they protect, the larger the MIC should be. For ephemeral keys that will be erased within minutes, a small MIC is sufficient. For a node’s long-term identity keys, prefer the full 16 bytes.

Frame Counter

The 4-byte frame counter must increase monotonically. UMSH uses this monotonic counter—rather than timestamps—for replay protection, keeping the protocol free of any dependency on synchronized clocks or absolute time.

A node address draws the counters of all its secured and signed frames—unicast, blind unicast, multicast, and signed broadcast—from one monotonic sequence, and MUST NOT let that sequence go backward (see Counter Persistence). A receiver judges every frame authenticated as a peer’s, whether pairwise or by a frame signature, against one baseline for that peer, so the sequence belongs to the sending address, not to a destination or channel.

Replay Detection

A receiver determines whether a frame counter is acceptable by computing:

delta = (received_counter - last_accepted_counter) mod 2^32

If delta is zero or exceeds the forward window, the packet is rejected. This modular comparison allows the counter to wrap around 2^32 without requiring special overflow handling. The suggested default forward window is 172800. Implementations MAY use a different value, but it should be large enough to accommodate gaps from packets sent to other destinations and small enough to limit the scope of replay attacks.

Implementations that need to tolerate out-of-order delivery may also define a backward window—a small range of counter values behind the highest accepted counter within which late-arriving packets are still considered. The suggested default backward window is 8. When a packet’s counter falls within the backward window, the receiver checks a small cache of recently accepted packets, keyed by frame counter and MIC (similar to the approach used for duplicate suppression in repeaters): if the packet is already present, it is a replay and is rejected; if not, the packet is accepted and added to the cache.

Regardless of window sizes, a packet must not be accepted if it is more than 5 minutes out of order—that is, if the highest accepted counter was last advanced more than 5 minutes ago and the received counter is behind it. MIC cache entries only need to be retained for the duration of this time bound. Additionally, the first packet accepted from a given node (or after a counter resynchronization) establishes that node’s counter baseline—packets with earlier counter values must be rejected, even if they arrive within the backward window.

Duplicate Acknowledgement Window

Rejecting a packet as a duplicate does not prohibit retransmitting its MAC acknowledgement. An authenticated packet that requests an acknowledgement and whose MIC identifies it as a previously received packet MAY be acknowledged again if its frame counter is no more than 8 counts behind the highest accepted counter for that traffic direction, inclusive. Equivalently:

ack_distance = (last_accepted_counter - received_counter) mod 2^32

The duplicate may be acknowledged only when ack_distance <= 8. A receiver MUST NOT acknowledge a replay farther behind the stored counter. Re-sending the acknowledgement is idempotent and allows a sender to recover when the original acknowledgement was lost; it does not accept the packet as new and MUST NOT advance or otherwise modify the stored replay baseline. A receiver may suppress duplicate delivery itself. In a split implementation, such as a companion radio and host MAC, it may instead pass the authenticated duplicate to the host, which is then responsible for suppressing duplicate application delivery.

Eligibility is not obligation: re-acknowledgement SHOULD be paced. Flood routing delivers one transmission as several copies, and a copy of a transmission whose acknowledgement was already sent proves nothing was lost. A receiver SHOULD NOT acknowledge the same packet more than once per forwarding-confirmation window, measured from its most recent acknowledgement of that packet: copies arriving inside the window share the acknowledgement already sent, while a sender that lost its acknowledgement cannot retransmit before its own confirmation window has lapsed.

A duplicate that carries the Route Retry option, of a packet already acknowledged and arriving after this window, shows that the acknowledgement was lost on its way back. Its re-acknowledgement SHOULD NOT follow the route the lost one took (Lost Acknowledgments). Any other late duplicate may have crossed an acknowledgement still in transit and says nothing about the route.

Payload acceptance does not begin the acknowledgement pacing window. A destination may accept an overheard source-routed copy while hints remain, withholding its ACK until the route is consumed. The first eligible copy may then be acknowledged immediately. A duplicate remains the same accepted packet: acknowledging it does not renew its replay-retention interval or deliver the payload again.

Signed-Frame Counters

A frame whose frame signature verifies is judged by the rules above against its signer’s counter, and is either accepted or dropped. The cache of recently accepted packets keys a signed broadcast, which has no MIC, by its frame counter and the first 16 bytes of its signature.

  • Known peer. A frame signed by a known peer is checked against that peer’s receive baseline, the one its unicast traffic is checked against. Because the peer draws every counter from one sequence, its signed multicast and broadcast frames continue the sequence its unicast frames follow.
    • An accepted frame advances the baseline, which is persisted as for unicast.
    • A replay is dropped.
    • A frame behind the baseline or outside the forward window is dropped. When the gap is too large for reordering to explain, the receiver MAY start counter resynchronization with an Echo Request, at the usual rate limit.
  • Stranger. A frame that verifies against a key that is not a known peer’s is first contact: it is accepted at face value, as Counter Resynchronization describes for any new peer. Later frames from that key are judged by the same rules, in a window the receiver keeps for that signing key. A frame behind that window’s baseline or outside it is dropped, with no resynchronization, since there is no pairwise relationship to run one over.
    • A receiver MAY discard a stranger’s window when it is idle or under memory pressure.
    • After a discard, or after the receiver restarts, the stranger’s next frame is first contact again, so a replay of one of its earlier frames is accepted once. With no prior knowledge of the sender, a receiver cannot tell that replay from a new frame.

Counter Persistence

How a node persists and recovers its own frame counter across reboots is implementation-specific. Possible strategies include writing the counter to non-volatile storage periodically or advancing the counter by a large margin on startup to avoid replaying previously used values.

A node MUST also persist each known peer’s receive baseline and restore it at startup. A node that lost a peer’s baseline would treat that peer’s next frame as first contact and accept a replay of any earlier one.

  • The persisted baseline MAY lag the live one, so that writes can be batched. A node SHOULD write a peer’s baseline once it has accepted 16 frames from that peer since the last write, however far those frames moved the counter. The frames accepted since the last successful write are what an unexpected restart leaves replayable.
  • Before a deliberate restart, a node SHOULD write every baseline that has changed since its last write.

Caution

If counters or baselines are written to non-volatile storage, care should be taken to avoid wearing out the underlying storage medium if it has a limited number of writes.

Counter Resynchronization

On first contact with a new peer, the received frame counter is accepted at face value and recorded as the baseline for future replay detection.

An authenticated packet from a known peer whose frame counter is far outside the window means the two nodes disagree about that counter—for example, because the peer rebooted and lost its persisted counter. Far outside means past the forward window, or far enough behind the baseline that reordering cannot explain it (the suggested threshold is 1024). The receiver MAY then re-establish a baseline with one of the two exchanges below. It SHOULD send at most one such probe to a peer every 5 seconds, and a Verify Frame or Frame Verified never prompts one.

When the packet requested an acknowledgement, the receiver—the requester—holds it without acknowledging it and sends Verify Frame to the peer, naming the packet in an Ack MIC option. The peer—the responder—proceeds as follows:

  1. It matches the option against the acknowledgement-requested packets it sent to the requester that are still awaiting acknowledgement. If none matches, it ignores the Verify Frame.
  2. It stops retransmitting the matched packet and answers with Frame Verified, which requests an acknowledgement.
  3. When Frame Verified is acknowledged, it treats the matched packet as acknowledged. If Frame Verified is never acknowledged, the matched packet has failed.

The responder processes a Verify Frame whose own counter falls outside its window for the requester, since the match with an outstanding packet already shows the request is fresh. When Frame Verified is acknowledged, it adopts that counter as the requester’s new baseline. A Verify Frame whose counter falls within the window is subject to the ordinary replay rules.

When a Frame Verified arrives echoing the nonce of a Verify Frame for a packet the requester still holds, the requester adopts its frame counter as the peer’s new baseline, whatever the value. It then delivers the held packet and acknowledges Frame Verified; that acknowledgement stands for the held packet too, which is not acknowledged separately. A requester that no longer holds the packet discards Frame Verified without acknowledging it, so the responder reports the packet as failed. The requester MUST choose nonces so that a Frame Verified recorded from an earlier exchange cannot match a later one.

When the packet did not request an acknowledgement, the peer has nothing outstanding to match, so the receiver MAY instead send an Echo Request carrying a nonce. An Echo Response echoing it establishes the response’s frame counter as the peer’s new baseline. The packet that prompted the probe is dropped.

Salt

The optional 2-byte salt is chosen randomly to reduce the likelihood of a nonce collision.

Cryptographic Processing

Unicast Key Agreement

For unicast and blind unicast:

  1. Start with sender Ed25519 keypair and recipient Ed25519 keypair.
  2. Convert both Ed25519 keys to X25519 form.
  3. Perform X25519 ECDH.
  4. Feed the resulting shared secret into HKDF-SHA256, bound to both parties’ Ed25519 public keys.
  5. Derive separate stable pairwise keys for encryption and MIC/authentication.

Ed25519 to X25519 Conversion

UMSH uses a single Ed25519 keypair per node as both its identity (for addressing) and the basis for key agreement. Standard cryptographic guidance recommends separate keys for signing and key agreement, so this choice warrants justification.

The Ed25519 and X25519 curves are birationally equivalent (both are defined over Curve25519), and the conversion between Edwards and Montgomery form is a well-understood, deterministic mapping. Using a single keypair for both purposes is not itself insecure—it is the approach taken by, among others, the Signal protocol’s X3DH key agreement and libsodium’s crypto_sign_ed25519_pk_to_curve25519 API.

The alternative—carrying separate Ed25519 (signing) and X25519 (key agreement) keys per node—would require a cryptographic binding between the two. Each node must distribute an additional 32-byte X25519 public key alongside its Ed25519 key, and the binding must be authenticated (e.g. by including the X25519 key in a signed advertisement). Every recipient must then verify that binding before trusting the key agreement key. On a LoRa link where the entire frame budget is ~255 bytes, even 32 extra bytes per identity exchange is a significant cost. By deriving X25519 keys from Ed25519 keys, UMSH eliminates this overhead entirely: the node address is the key agreement key, with no additional key distribution required.

UMSH assumes standard Edwards-to-Montgomery conversion:

  • sender Ed25519 private key → sender X25519 private key
  • sender Ed25519 public key → sender X25519 public key
  • recipient Ed25519 private key → recipient X25519 private key
  • recipient Ed25519 public key → recipient X25519 public key

Implementations MUST reject malformed public keys before conversion. A public key is accepted only if re-encoding its decoded point yields the same 32 bytes, which rejects non-canonical encodings of a point.

ECDH Shared Secret

The ECDH shared secret is:

ss = X25519(local_x25519_private, remote_x25519_public)

This shared secret is used as the input keying material for deriving the cryptographic keys to secure and authenticate messages.

HKDF Inputs for Unicast

For unicast packets, the encryption and MIC keys are derived from the X25519 ECDH shared secret and both nodes’ Ed25519 public keys, and are stable for a given pair of nodes. These keys are not derived from packet-specific fields.

Let:

ss = X25519(local_x25519_private, remote_x25519_public)

The pairwise key material is then derived as:

ikm  = ss
salt = "UMSH-PAIRWISE-SALT"
info = "UMSH-UNICAST-V3" || min(PK_a, PK_b) || max(PK_a, PK_b)
okm  = HKDF-SHA256(ikm, salt, info, 64)

PK_a and PK_b are the two nodes’ 32-byte Ed25519 public keys, ordered by comparing their encodings lexicographically, so both ends derive the same keys. Binding the full keys gives each key its own pairwise keys: an Ed25519 key and its negation convert to the same X25519 key and so produce the same shared secret, but not the same info.

The output keying material is split as follows:

K_mic = okm[0..32]
K_enc = okm[32..64]

Where:

  • K_mic is the 32-byte message authentication key (the S2V key)
  • K_enc is the 32-byte encryption key (the CTR key)

This layout makes okm exactly the AES-SIV key K from RFC 5297: K_mic is the leftmost half K1 (S2V) and K_enc is the rightmost half K2 (CTR), so okm can be handed directly to an AEAD_AES_SIV_CMAC_512 implementation.

These keys are stable for the sender/recipient pair and may be cached by the implementation. They do not change from packet to packet.

Because the key derivation depends only on the ECDH shared secret, the two public keys, and fixed UMSH-specific HKDF parameters, it does not need to be recomputed for each transmitted packet.

Blind Unicast Payload Keys

Blind unicast payload encryption and authentication must require knowledge of both the pairwise shared secret and the channel key. This ensures that an attacker who compromises one of the two secrets—but not both—cannot decrypt blind unicast payloads.

The blind unicast payload keys are derived by XORing the pairwise unicast keys (see HKDF Inputs for Unicast) with the channel’s multicast keys (see Multicast Packet Keys):

K_enc_blind = K_enc_pairwise XOR K_enc_channel
K_mic_blind = K_mic_pairwise XOR K_mic_channel

Where:

  • K_enc_pairwise, K_mic_pairwise are the stable pairwise keys derived from the sender/recipient ECDH shared secret
  • K_enc_channel, K_mic_channel are the stable channel keys derived from the channel key

Both sets of input keys are independent HKDF outputs—pseudorandom and uncorrelated. XOR of two independent uniform random values is uniform random: an attacker who knows only one side sees the combined key as informationally equivalent to a one-time pad over the unknown side.

These combined keys are stable for a given (sender, recipient, channel) triple and may be cached. If the same two nodes use blind unicast over different channels, they get different payload keys—compromise of one channel key does not expose blind unicast traffic on another channel between the same pair.

Both the pairwise and channel keys can be cached independently by the implementation. Computing the blind unicast keys requires only a 32-byte XOR per key, with no additional HKDF calls.

Per-Packet Security Inputs

Although K_enc and K_mic are stable for a given node pair, each packet still carries per-packet security inputs in SECINFO.

These inputs are:

  • the 4-byte frame counter
  • the optional 2-byte salt

These values are not used to derive the pairwise keys. Instead, they are used as packet-specific inputs to encryption, authentication, and replay protection.

For encrypted packets:

  • K_enc and K_mic are the stable pairwise keys
  • SECINFO and other immutable header fields are supplied as associated data
  • the frame counter and optional salt provide packet-specific variability and replay-detection context

For authenticated but unencrypted packets:

  • K_mic is the stable pairwise MIC key
  • the MIC is computed over the protected packet contents and relevant static fields

The frame counter must increase monotonically for a given traffic direction. Receivers must not rely on the construction’s resistance to nonce misuse as a substitute for replay detection.

Multicast Packet Keys

For multicast, the configured channel key is the base secret. The encryption and authentication keys are derived once and are stable for a given channel.

ikm  = channel_key
salt = "UMSH-MCAST-SALT"
info = "UMSH-MCAST-V2" || channel_id
okm  = HKDF-SHA256(ikm, salt, info, 64)

K_mic = okm[0..32]
K_enc = okm[32..64]

As with the pairwise keys, okm is exactly the RFC 5297 AES-SIV key for the channel: S2V key first, CTR key second.

These keys are stable for the channel and may be cached by the implementation. They do not change from packet to packet. Per-packet variability is provided by the frame counter and optional salt in SECINFO, which serve as inputs to encryption and replay detection (see Per-Packet Security Inputs).

Encrypted Packets

When encryption is enabled, UMSH uses AES-SIV (RFC 5297) with AES-256.

The processing is:

  1. Compute the full 16-byte synthetic IV V = S2V(K_mic, S1, S2) per RFC 5297 §2.4, where S1 is the canonical associated data (see Associated Data) and S2 is the plaintext.
  2. Truncate V to the MIC length specified by the SCF; this is the on-wire MIC.
  3. Construct the CTR IV from the MIC (see CTR IV Construction).
  4. Encrypt the plaintext using AES-256-CTR with K_enc and the constructed IV. The counter is the full 16-byte IV, incremented as a 128-bit big-endian integer (RFC 5297 §2.5).

The plaintext S2 is the region of the packet after its options and before its MIC:

FamilyS2
UnicastPAYLOAD [‖ SIG]
MulticastSRC ‖ PAYLOAD [‖ SIG]
Blind unicastPAYLOAD [‖ SIG]

SIG is the frame signature, present when the SCF’s D bit is set. Blind unicast’s address block is not part of S2: it is covered through the associated data and encrypted separately (see Blind Unicast Address Encryption).

The MIC is transmitted at the end of the packet rather than prepended as in RFC 5297, allowing its length to be controlled independently via the SCF MIC size field.

With a 16-byte MIC, the result is byte-for-byte AEAD_AES_SIV_CMAC_512 (RFC 5297 §6.3): the key is K = K_mic || K_enc, the canonical AAD is the single associated-data component, and the CTR IV construction reduces to the RFC’s initial counter Q. Shorter MIC lengths use the same computation and differ only in how the CTR IV is filled.

As in RFC 5297, the synthetic IV binds the ciphertext to the key, associated data, and plaintext, so there is no caller-supplied nonce to misuse: repeating all three inputs yields an identical packet and reveals only the repetition itself. Truncated MICs weaken this bound in proportion to the truncation, since the CTR IV is reconstructed from the truncated MIC and SECINFO rather than the full V.

CTR IV Construction

The 16-byte CTR IV is constructed by appending the SECINFO field to the MIC, zero-padding or truncating the result to exactly 16 bytes, and then clearing the top bit of bytes 8 and 12 as RFC 5297 §2.6 does when forming the initial counter:

IV     = truncate_or_pad_to_16( MIC || SECINFO )
IV[8]  = IV[8]  & 0x7F
IV[12] = IV[12] & 0x7F

The bit-clearing step is applied unconditionally, whatever bytes—MIC, SECINFO, or zero padding—occupy positions 8 and 12.

For the 16-byte MIC, SECINFO is entirely truncated away and the IV is the masked synthetic IV—exactly the initial counter Q from RFC 5297 §2.6. For shorter MICs, the IV incorporates the frame counter and optional salt from SECINFO, providing additional per-packet IV variability.

MIC LengthSECINFO (5 B)SECINFO (7 B)SECINFO bytes in IV
16 Btruncate to 16truncate to 160 (IV = masked MIC)
12 Btruncate to 16truncate to 164
8 Bzero-pad to 16zero-pad to 165 or 7
4 Bzero-pad to 16zero-pad to 165 or 7

Unencrypted Packets

When encryption is disabled, the MIC is computed exactly as for encrypted packets—the truncated S2V(K_mic, S1 = AAD, S2), with the same S2—and the encryption step is simply omitted. An unencrypted multicast’s SRC is therefore covered as part of S2, just as when it is encrypted.

Associated Data

The associated data (AAD) binds the immutable header fields to the MIC so that any modification is detected. The canonical AAD enters S2V as the single associated-data component S1; the plaintext S2 (see Encrypted Packets) is the final component.

The AAD is the packet’s header fields in wire order, with these differences:

  • FHOPS is omitted, and the FCF enters with its flood-hops-present (H) bit cleared.
  • The static options are re-encoded as type-length-value triples (see below) and dynamic options are omitted.
  • The options are followed by the 2-byte terminator FF FF, which is present even when there are no static options.
  • Blind unicast’s address block, DST ‖ SRC, follows the terminator in plaintext form, whether or not it is encrypted on the wire.
  • Multicast’s SRC is not part of the AAD. It is part of S2, with either value of E.

Addresses enter as they appear on the wire: a hint as a 3-byte hint, and a full key only where the packet carries one.

FamilyAAD
BroadcastFCF ‖ SRC ‖ options ‖ FF FF
MAC ack (AAD_ack)FCF ‖ options ‖ FF FF
UnicastFCF ‖ DST ‖ SRC ‖ SECINFO ‖ options ‖ FF FF
MulticastFCF ‖ CHANNEL ‖ SECINFO ‖ options ‖ FF FF
Blind unicastFCF ‖ CHANNEL ‖ SECINFO ‖ options ‖ FF FF ‖ DST ‖ SRC

No AAD depends on E: two packets that differ only in E have AADs that differ only in the SCF byte. A broadcast has no MIC; its AAD is used only by a frame signature.

The AAD is unambiguous because the packet format is self-describing from left to right. The FCF decides which fields are present, and its S bit gives SRC’s length; SECINFO’s own SCF gives its length. On the wire the options block is delimited by its 0xFF end-of-options marker, which the type-length-value re-encoding drops; the FF FF terminator restores that delimiter. No static option can begin with it: option number 0xFFFF has both low bits set, so it is dynamic and never enters the AAD, and no larger option number exists (see Option Encoding). The AAD therefore fixes its own end.

Dynamic options and the flood hop count are excluded from the AAD because they may be modified by repeaters during forwarding. The FCF’s flood-hops-present (H) bit is part of that budget rather than of the packet’s identity, and is cleared before the byte enters the AAD: a sender abandoning a source route re-floods a packet that is already sealed, which adds FHOPS where the original had none. Masking the bit does not weaken the binding. Flipping it on the wire shifts every field the parser reads after it, so the fields that follow enter the AAD as different values and the MIC check still fails.

A MAC Ack has no DST, CHANNEL, SRC, or SECINFO field, so its AAD is the FCF, static options, and terminator alone (see Ack Tag Construction). For an ack, masking H is what lets a re-sent acknowledgement, which may differ from the first only in its flood budget, carry the same tag.

Static Option Encoding in AAD

Static options are not included in their wire delta-length form. Instead, each static option present in the packet is re-encoded using absolute type-length-value triples:

+----------+----------+-------+
|  number  |  length  | value |
+----------+----------+-------+
    2 B (BE)   2 B (BE)  var.

Where number is the option’s absolute option number (not a delta), encoded as a 2-byte big-endian unsigned integer, and length is the value length in bytes, also encoded as a 2-byte big-endian unsigned integer. Static options appear in the AAD in order of increasing option number. This avoids recomputing deltas after dynamic options have been removed.

Using 2-byte fields for both number and length ensures that option numbers above 255 (which are valid in the CoAP-style encoding used on the wire) and long option values are represented without truncation or ambiguity. A packet that names an option number above 0xFFFF is malformed (see Option Encoding).

Ack Tag Construction

When a packet type requests an acknowledgement (UNAR or BUAR), the acknowledgement carries two 4-byte fields: an ack MIC for correlation and an ack tag for authentication.

The ack MIC is simply the first 4 bytes of the original packet’s on-wire MIC:

ack_mic = first_4_bytes( on_wire_MIC )

It is public—any node that received the original packet, including forwarding repeaters, can compute it. Its purpose is correlation: it lets the original sender match the ack to the outstanding request it belongs to, and lets a repeater that forwarded the original packet recognize the ack as its acknowledgement (enabling passive-ack optimizations). It is not an authenticator.

The ack tag is the MIC of the acknowledgement itself. Only the original sender and the final destination can produce it, and it covers the acknowledgement’s static options as well as the packet it answers:

ack_tag = truncate_to_4( S2V( K_mic, S1 = AAD_ack, S2 = V, S3 = ack_mic ) )

Where:

  • K_mic is the MIC key that sealed the original packet—the pairwise key for unicast (see HKDF Inputs for Unicast), or the combined blind unicast key for blind unicast (see Blind Unicast Payload Keys)
  • V is the original packet’s full 16-byte S2V output, before truncation to its on-wire MIC length
  • AAD_ack is the acknowledgement’s associated data: its FCF with the H bit cleared, followed by its static options and the FF FF terminator

The three components take the shape of any other packet’s MIC: associated data first, V where a nonce would sit, and ack_mic as the payload. The component count and the acknowledgement’s own FCF keep the result distinct from every packet MIC computed under the same key. Dynamic options and the flood hop count stay outside AAD_ack, so forwarding does not disturb the tag.

Binding V ties the tag to one exchange in one direction. Both peers hold the same pairwise keys, but an acknowledgement of a packet one peer sent cannot pass for one answering a packet the other sent. V need not be secret for this; a packet sent with a 16-byte MIC carries all of it.

The standalone MAC Ack carries both fields (ack_mic followed by ack_tag, 8 bytes total). The Ack MIC option carries only ack_mic, because the packet carrying the option is itself authenticated to the original sender and therefore needs no separate keyed tag.

The ack_mic is a prefix of the original packet’s on-wire MIC, so it was already visible to anyone who received that packet. Producing the ack_tag requires K_mic, so an observer who intercepts the original packet can neither forge a standalone ack nor alter the static options of a genuine one. With a 4-byte tag, a blind forgery succeeds with probability 2^-32 per attempt, and only while the original packet still awaits acknowledgement: no offline check exists, each guess costs a transmission, and a failed guess draws no response. A successful forgery would cause the sender to treat an undelivered packet as delivered and suppress retransmission, or to act on options the destination never sent—a reliability denial-of-service confined to one exchange, not a confidentiality break. A weaker variant needs no forged tag at all: repeaters cancel queued forwards on the public ack_mic alone, so an observer who saw the original packet can suppress its pending forwards at repeaters within earshot. The exposure is the same reliability class, costs the attacker a transmission per suppression, and is bounded by the sender’s Route Retry recovery.

The correlation exposed by ack_mic is deliberate. To an observer who already received the original packet, it confirms that the packet was delivered and links the ack to that packet. Because the MAC ack carries no destination hint, it adds no explicit endpoint identifier of its own—which removes the direct sender-identity leak a destination hint would introduce. This is not a guarantee of unlinkability: a determined adversary may still correlate an ack with its endpoints through timing, RF fingerprinting, return-path analysis, or by tying the ack_mic back to an original packet that itself exposed endpoint hints. For blind unicast, whose forward frame reveals no endpoint identity to a non-channel observer, omitting the destination hint keeps that concealment from being undone by the ack.

Blind Unicast Address Encryption

Blind unicast packets encrypt both the destination hint and source address together, separately from the payload. The address block is encrypted with the channel key alone, so that any channel member can recover both the intended recipient and the sender’s identity. The payload is then encrypted with the combined blind unicast keys (see Blind Unicast Payload Keys), which require both the channel key and the pairwise shared secret.

The address block is encrypted using AES-256-CTR with the channel’s derived encryption key K_enc_channel (see Multicast Packet Keys), using the CTR IV constructed from the packet MIC and SECINFO (see CTR IV Construction).

Let:

  • K_enc_channel = channel encryption key derived from the channel key via HKDF
  • IV = CTR IV constructed from the packet MIC and SECINFO
  • DST = 3-byte destination hint
  • SRC = source address: 3-byte source hint when S=0, or 32-byte source public key when S=1

Then:

ENC_DST_SRC = AES-256-CTR( key=K_enc_channel, iv=IV, plaintext=DST || SRC )

This allows a receiver possessing the channel key to recover both the destination (to confirm the packet is addressed to them) and the source address (to derive the pairwise keys needed to authenticate and decrypt the payload).

The address block is covered by the MIC. DST ‖ SRC enters the associated data in plaintext form, so the sender computes the MIC over the plaintext addresses and only then encrypts the block with the IV that MIC yields. The receiver decrypts the block from the on-wire MIC and the channel key, which it can do before it knows the payload keys, then derives the payload keys from the recovered SRC and verifies the MIC. Any change to the encrypted block changes the recovered addresses, and the MIC fails. Because the MIC covers the addresses it was derived from, the address block’s IV is a true synthetic IV.

Frame Signatures

A frame signature is an Ed25519 signature by the sender’s node key over the bytes the MIC covers. It attributes a frame to a key: a verified signature shows that the holder of that key sent this frame, with this header and frame counter, on this channel. A channel MIC alone shows only that the sender holds the channel key.

The signature is carried in one of two ways:

  • Frames with SECINFO (unicast, multicast, blind unicast): the SCF’s D bit is set, and a 64-byte SIG follows the payload and precedes the MIC. SIG is the last part of S2, so the MIC covers it, and with E set it is encrypted with the payload.
  • Broadcast: the Frame Signature option carries a frame counter and the signature.

Signature Input

The signed bytes are:

signature_input = "UMSH-SIG" || AAD || [CHANNEL_COMMITMENT] || plaintext

Where:

  • "UMSH-SIG" is the 8 ASCII bytes 55 4D 53 48 2D 53 49 47. It is never transmitted.
  • AAD is the frame’s associated data, with the SCF’s D bit set. For a broadcast, the Frame Signature option enters the AAD with its 64 signature bytes replaced by zeros; its frame counter enters as sent.
  • CHANNEL_COMMITMENT is present only on multicast and blind unicast (see Channel Commitment).
  • plaintext is S2 without SIG, in plaintext form whether or not it is encrypted on the wire:
Familyplaintext
BroadcastPAYLOAD
UnicastPAYLOAD
MulticastSRC ‖ PAYLOAD
Blind unicastPAYLOAD

A multicast’s SRC is not in its AAD, so it enters the signature input at the front of the plaintext. The SRC that names the signing key is therefore always covered by the signature.

The input is unambiguous without length prefixes: the label has a fixed length, the AAD fixes its own end, the FCF inside the AAD decides whether a 32-byte commitment follows, and the plaintext is everything after that.

Channel Commitment

The CHANNEL field carries only a 2-byte channel identifier, which distinct channel keys can share. Without a commitment, someone holding two channel keys with the same identifier could move a signed frame from one channel to the other and recompute its MIC, and the signature would still verify on a channel its sender never signed for. The channel commitment binds the signature to the channel key:

CHANNEL_COMMITMENT = first_32_bytes( HKDF-SHA256(ikm = channel_key, salt = "UMSH-CHAN-ID", info = "") )

This is the same derivation as the channel identifier, extended to 32 bytes; the 2-byte channel_id and the 16-byte channel identifier are prefixes of it. The commitment is never transmitted. A receiver computes it from the channel key that verified the frame’s MIC, so when channel identifiers collide the signature is checked against the channel whose key authenticated the frame, never one matched by identifier alone.

HKDF is one-way, so a receiver can disclose a channel’s commitment to let a third party verify an unencrypted signed multicast without disclosing the channel key.

Verification

A receiver verifies a frame signature against the signer’s public key: the full SRC when the frame carries one, or the key of the one known peer whose hint matches SRC. A hint-only signed frame can be verified only by a receiver that already holds the sender’s key.

  • A frame whose signature verifies is attributed to that key, and its counter is judged by Signed-Frame Counters.
  • A frame whose signature does not verify, or whose key cannot be resolved, is not attributed to any key. It is processed as an unsigned frame from the same source would be. A failed verification is not by itself grounds to drop the frame: a hint can match a different known peer than the one that signed.

The signature does not change how a frame is authenticated. A secured frame’s MIC is verified as usual, and a frame whose MIC fails is dropped before its signature is considered.

Perfect Forward Secrecy Sessions

UMSH provides optional perfect forward secrecy (PFS) via ephemeral node addresses exchanged using the PFS Session MAC commands. Once a PFS session is established, traffic between the two nodes is encrypted and authenticated exactly as if the ephemeral addresses were any other long-term node identities. Compromise of either node’s long-term private key cannot retroactively expose traffic encrypted during a PFS session, because the private keys for the ephemeral addresses are erased when the session ends and the session’s ECDH shared secret is irrecoverable.

Handshake

A PFS session is established via a two-message exchange over the existing authenticated unicast channel:

  1. Initiator: Generates a fresh ephemeral node address. Sends a PFS Session Request carrying the new ephemeral address and a requested session duration. The request is authenticated with the initiator’s long-term keys.

  2. Responder: Generates its own fresh ephemeral node address. Sends a PFS Session Response carrying the responder’s ephemeral address and the accepted session duration. The response is authenticated with the responder’s long-term keys.

After this exchange, both sides hold each other’s ephemeral addresses and can independently derive the session keys. No further setup messages are required. The first data packet sent by the initiator using the ephemeral address hints serves as an implicit acknowledgement to the responder that the response was received and the session is active.

Session Key Derivation

A PFS session is cryptographically identical to a normal UMSH unicast session in every respect—the only difference is that the participating node addresses are ephemeral rather than long-term. Key derivation follows the exact same process as Unicast Key Agreement.

The PFS property arises not from any difference in how the keys are derived, but from the fact that the private keys for the ephemeral addresses are never stored durably and are securely erased when the session ends.

An ephemeral node address is a fully functional temporary UMSH node identity: it has an address hint, can be addressed directly, and its private key is converted to X25519 for ECDH the same way a long-term identity is.

Because a PFS session is indistinguishable from an ordinary unicast session at the MAC layer, it requires no changes to MAC-layer processing, no changes to any application-layer protocol, and adds zero per-packet overhead. Once the two-message handshake completes, every subsequent packet in the session is exactly the size it would have been without PFS.

Wire-Level Privacy

While a PFS session is active, packet hint fields are derived from the ephemeral node address rather than the long-term address. A passive observer sees packets flowing between two unfamiliar node IDs that appear only for the duration of the session. The only packets that expose the long-term node IDs are the two handshake messages (PFS Session Request and PFS Session Response), which are themselves protected by the long-term pairwise keys.

Because ephemeral node addresses are structurally identical to long-term node addresses, an observer cannot distinguish PFS session traffic from ordinary unicast traffic, nor associate the ephemeral addresses with the original nodes that created the session.

This identity separation is not unconditional. The PFS handshake messages are authenticated with the nodes’ long-term keys, so an attacker who later compromises a long-term private key can retroactively identify which long-term identities established the session—even though the session’s content remains protected by the erased ephemeral keys.

Additionally, implementations that use a single device-wide frame counter expose a correlation opportunity: an observer who can read the frame counter field across packets (e.g. by receiving a packet before and after the PFS handshake) may notice continuity in the counter value and link the ephemeral addresses to the originating nodes. Implementations that wish to preserve wire-level identity unlinkability should use independent frame counters for each node address—including ephemeral ones—so that session traffic is not correlated with long-term traffic through counter continuity.

From the application layer’s perspective, the implementation maps the ephemeral identity back to the originating long-term node ID throughout the session, so applications continue to see communication with the same peer they initiated the session with.

Session Lifetime

A PFS session ends when any of the following occur:

  • The agreed session duration expires.
  • Either party sends an End PFS Session command.
  • Either device reboots.

Upon session end, both sides must securely erase/zeroize the private keys for their ephemeral addresses. Without those private keys, the session’s ECDH shared secret cannot be reconstructed, and the session traffic cannot be decrypted even by an attacker who later obtains the long-term private keys. This erasure is the mechanism that provides forward secrecy.

Caution

Implementations must ensure that the private keys for ephemeral addresses are not swapped to disk, written to logs, or otherwise persisted in any form. On embedded platforms, this requires explicitly zeroing the key material in RAM before releasing it. Failure to securely erase these keys eliminates the PFS property entirely.

Routing Overview

UMSH packets can be delivered directly to nodes within radio range, flooded across the mesh, source-routed through a specific sequence of repeaters, or delivered using a hybrid of source routing and flooding. This chapter gives a high-level picture of how these mechanisms fit together; detailed procedures are covered in the sections linked below.

In general, it is the responsibility of the individual endpoints to properly route their traffic to their destination.

Direct (Single-Hop) Delivery

The simplest case: the sender transmits a packet with no source-route option and no flood hop count. Only nodes within direct radio range will receive it. No repeater forwarding occurs. This is appropriate when the destination is known to be a direct neighbor, or for local broadcasts and beacons that do not need multi-hop propagation.

Flood Routing

The sender sets a flood hop count in the packet header, and every repeater that receives the packet decrements the remaining count, increments the accumulated count, and retransmits. The packet radiates outward until the hop count is exhausted or all reachable repeaters have forwarded it.

Flood routing requires no prior knowledge of the network topology. It is used for broadcasts, multicast, and unicast when no source route is known. The cost is airtime: every repeater in range participates, so a high hop count can saturate a busy mesh.

See Packet Structure § Flood Hop Count for encoding details and Repeater Operation § Forwarding Procedure for the forwarding rules.

Region Scoping

The region code option restricts flood forwarding to repeaters configured for that specific geographic region. A repeater that does not recognize or is not configured for the region MUST NOT flood-forward the packet. If multiple region code options are present, matching any one of them is sufficient for flood forwarding. If a packet is being flood-forwarded without a region code, a repeater may add one according to local policy, but it must never rewrite an existing region code or add a second one. Region scoping is not enforced during the source-routed portion of a hybrid route—only after the source-route hints are exhausted and the packet transitions to flooding.

See Packet Options § Region Code.

Signal-Quality Filtering

Two packet options let the sender control which links are acceptable for flood forwarding:

  • Minimum RSSI—a repeater that received the packet below the specified signal strength must not flood-forward it.
  • Minimum SNR—a repeater that received the packet below the specified signal-to-noise ratio must not flood-forward it.

These thresholds prevent retransmission over weak links that are unlikely to deliver the packet reliably, saving airtime and transmit power. The repeater may also enforce its own local thresholds; the effective threshold is the higher of the two.

See Packet Options § Minimum RSSI and Packet Options § Minimum SNR.

Source Routing

When the sender knows a path to the destination, it can include a source-route option listing the sequence of repeater hints the packet should traverse. Each repeater checks whether it matches the next hint, removes its own hint, and forwards. Only the designated repeaters handle the packet, so source routing avoids the airtime cost of flooding.

Source routes are learned from the Trace option: when a packet carries one, each forwarding repeater prepends a record of itself, on routed hops as well as flooded ones. The recipient can cache the router hints of a complete trace as a source route for future replies. This means path discovery is not a separate operation—it falls out of normal packet exchange.

Knowing the path forward is not the same as the destination knowing the path back. A source-routed packet arrives with its hints consumed, so nothing on it describes the return direction, and a destination with no cached route composes a reply that no repeater may carry. A sender therefore includes a Trace option whenever the destination may not hold a working path back; the routed hops record themselves, and the reply or acknowledgement has a path home. See When to Include a Trace.

See Packet Options § Source Route, Packet Options § Trace, and Beacons & Path Discovery § Route Learning.

Hybrid Routing

A packet can carry both a source-route option and a flood hop count. The packet is source-routed through the listed repeaters first; once the source-route hints are exhausted, it transitions to flood routing bounded by the remaining flood hop count. This enables “deliver to a region, then flood locally” behavior—useful for reaching a node in a known area without flooding the entire mesh.

See Repeater Operation § Routing Implications.

Bridging

A bridge is a node that relays UMSH packets between two different media or RF channels—for example, from a local LoRa radio to an internet backhaul and back to a distant LoRa radio, or between two radio bands.

Bridges are not prohibited per-se, as that is not a protocol-level decision. Instead, this document provides some guidance on how bridges can be deployed while lowering the risk of hurting local mesh performance.

Bridges are largely protocol-transparent. A bridge carries a packet between the repeaters at either end of it, and those repeaters do the forwarding: each consumes source-route hints, accounts for a hop, and records itself in a trace exactly as it would for a packet it heard off the air. A trace that crosses a bridge therefore contains records for both of them, and source-routed packets traverse the bridge transparently.

Because a repeater at each end forwards the packet, a crossing spends two flood hops rather than one. The repeater on the inbound side transmits on the medium the packet arrived from, which is the ordinary forwarding confirmation the previous hop listens for; a bridge needs no retransmission of its own to provide it.

Flooding works across bridges, and the two hops a crossing spends are what keep individual meshes local and accountable while still enabling multi-segment routing. A bridge MAY additionally clamp the remaining flood hop count of packets that transit it, which lets an operator narrow a bridge’s reach without reconfiguring the nodes behind it.

Caution

Internet bridges have the potential to be destructive to the mesh and are generally discouraged because 1) they cannot be relied upon in an emergency, and 2) they can waste airtime with useless, non-local chatter. Moreso than other types of bridges, internet bridges MUST limit what transits them: the flood hop count is bounded by the two hops a crossing spends, and deployments SHOULD rate-limit each participant and MAY clamp the hop count further.

A client–server tunnel realization of an internet bridge is specified in Internet Bridging. A node with a LoRa radio and its repeater enabled that joins an IP multicast group is a bridge between the group and its LoRa channel.

Forwarding Confirmation and Recovery

UMSH provides hop-by-hop forwarding confirmation for both source-routed and flood-originated packets. After transmitting, a node listens for the next hop to retransmit the same packet. If no retransmission is heard within a timeout, the node retries after a short jittered delay (up to 3 retries). The original sender does not currently receive any notification of a forwarding failure when source routing.

If a cached route fails entirely (noticed because of a timeout), the sender can fall back to flood routing for the same logical packet using the route retry option, which allows repeaters to forward it even if they already suppressed the original attempt.

A cached source route is the visible case, but not the only one. A sender that believes the destination is directly reachable, or reachable within a known flood distance, narrows FHOPS accordingly and carries no option recording that it did so. When such a packet goes unacknowledged, the cached distance is as stale as a dead source-route hint, and the recovery transmission restores the flood budget the sending application originally allowed. A radius the application itself chose is not a stale assumption and is not widened.

A packet that flooded at its whole budget may have failed on the way back instead: the destination’s route to the sender may be dead, and a MAC ack is never itself acknowledged. A sender that has heard from the destination recently therefore sends one Route Retry even then, at the same budget. When that retry reaches a destination that already acknowledged the packet, the destination retires the route its acknowledgment took and re-acknowledges by a scoped flood with a trace. Each logical packet gets at most one Route Retry.

See Repeater Operation § Forwarding Confirmation and Repeater Operation § Route Failure Recovery.

Channel Access

Before any transmission—original, forwarded, or acknowledgment—a node performs Channel Activity Detection (CAD) and backs off if the channel is busy. Flood-forwarding repeaters additionally use a contention window based on received SNR and RSSI, so that better-positioned repeaters transmit first and the rest can suppress their retransmission if they overhear an earlier forward.

See Channel Access.

Packet Processing

This chapter describes how a receiving node processes an incoming packet. This procedure applies to all nodes, not just repeaters. Repeater-specific forwarding logic is described separately in Repeater Operation.

A packet enters this procedure however it reached the node. Most arrive off the air, but a node whose radio is shared with another stack also receives what that stack transmits, and a node reachable over a point-to-point link—a backhauled host, for instance—receives what arrives across it. A node with a LoRa radio that is also in an IP multicast group receives what other nodes send to the group. Such a packet is processed exactly as a received one, with one difference: it carries no signal measurements, so anything defined in terms of how well the packet was heard does not apply to it.

Receive Procedure

  1. Well-formedness check

    • If the packet is malformed (invalid FCF, truncated fields, non-zero reserved bits in the SCF), drop.
  2. MAC Ack handling

    • If the packet is a MAC Ack:
      • If this node was not expecting the ack, stop.
      • Otherwise, handle the ack and stop.
  3. Address matching

    • If the packet is a broadcast, continue.
    • If the packet carries a destination hint that matches this node, continue.
    • If the packet carries a channel hint matching a configured channel, continue.
    • Otherwise, stop.

    A node always attempts to handle a packet that matches its destination hint, even if the packet has remaining source-route hops. This differs from systems like MeshCore and allows two nodes that are suddenly in direct range of each other to recover quickly without waiting for the packet to traverse the full source route. A node acting as a repeater does not also forward such a packet: it is the destination, and the packet has arrived (see Routing Invariants).

  4. Channel processing (multicast and blind unicast)

    4.1. If the packet is a blind unicast:

    • Decrypt the source address using the channel’s derived encryption key K_enc.
    • If the source address is a hint (S=0), look up candidate public keys matching the hint. If no candidates exist, stop.

    4.2. Attempt decryption and MIC verification for each candidate channel key.

    • For blind unicast, this may require re-decrypting the source address for each candidate channel from step 4.1.
    • If no candidate channel succeeds, stop.
  5. Unicast processing

    5.1. If the source address is a hint (S=0), look up candidate public keys matching the hint. If no candidates exist, stop.

    5.2. Attempt MIC verification (and decryption if encrypted) for each candidate source address.

    • If no candidate succeeds, stop.

    5.3. If the source address is blacklisted, drop.

  6. Payload type and option validation

    • If the payload type is not allowed for this packet type (see Payload and Packet Type Compatibility), drop.
    • If the packet carries a delivery-critical option this node does not recognize, or whose value it cannot interpret, drop it and count the drop. The packet is not accepted, delivered, or acknowledged.
  7. Replay and Ack Processing

    • If the packet carries a frame signature, verify it. A packet whose signature verifies is judged by the signed-frame counter rules; one whose signature does not verify continues as an unsigned packet.
    • Apply the replay-detection rules.
    • If the packet is accepted as new and requests an ACK, the receiving node (i.e., the final destination) computes the ack MIC and ack tag—the first 4 bytes of the original packet’s MIC and a 4-byte MIC under the pairwise K_mic over the ack’s own static options, bound to the original packet’s full 16-byte S2V output—prepares a MAC Ack packet, and adds it to the outbound queue. Repeaters do not generate acks—see MAC Ack Packet.
    • If the authenticated packet is a previously received packet within the bounded duplicate-acknowledgement window, the receiver MAY prepare and queue its ACK again. This does not make the packet new or advance its replay state. Unless duplicate suppression is delegated to a host MAC, stop after queueing the ACK and do not process the application payload again.
    • A Verify Frame or Frame Verified whose counter falls outside the window is processed as Counter Resynchronization describes rather than dropped.
    • Otherwise, if replay detection rejects the packet, drop it without acknowledging it.
  8. Application processing

    • Continue processing the application payload.

Channels

A channel is a named communication context defined by a shared symmetric key. Possession of the channel key grants membership and enables two distinct roles in UMSH:

  • Multicast—any node that possesses the channel key can send and receive packets addressed to the channel, enabling group communication.
  • Blind unicast—the channel key conceals both sender and destination addresses on the wire, while the payload itself is protected end-to-end using combined keys that require both the channel key and the pairwise shared secret. The channel serves as a metadata-concealment layer; the payload is readable only by the intended recipient, not by all channel members. See Blind Unicast Packet and Blind Unicast Address Encryption for details.

In both cases, the channel key is the membership credential—possessing it is both necessary and sufficient to participate.

Channel Keys

A channel key is a 32-byte symmetric key. It serves as the root secret from which encryption, authentication, and identification keys are derived (see Multicast Packet Keys).

How a node obtains a channel key depends on the type of channel—see Joining a Channel below.

Channel Identifier

Each channel is identified on the wire by the first 2 bytes of the 16-byte channel identifier derived from the channel key. What travels on the wire is a compact hint that allows receivers to quickly identify candidate channels without attempting decryption with every configured key. Like destination hints, it is not cryptographically authoritative—collisions are possible and must be resolved by attempting cryptographic verification. The full identifier is where a collision cannot be tolerated: it names a channel to a management interface without disclosing the key.

Encrypted and Unencrypted Modes

Channel-addressed packets (multicast and blind unicast) may be sent with or without encryption, controlled by the E flag in the Security Control Field.

  • Encrypted (E=1): The source address is encrypted together with the payload, concealing the sender’s identity from observers who do not possess the channel key. Only channel members can recover the source address and payload. See Encrypted Multicast for the packet layout.

  • Unencrypted (E=0): The source address and payload appear in cleartext, but the packet is still authenticated with a MIC derived from the channel key. This mode is useful for amateur radio operation or other contexts where encryption is not permitted. See Unencrypted Multicast for the packet layout.

Multi-Hop Delivery

Channel-addressed packets are delivered via flood forwarding, bounded by the optional flood hop count field. Repeaters forward these packets according to the standard forwarding procedure, including duplicate suppression, signal-quality filtering, and region-scoped flooding.

Sender Authentication

Multicast authentication is based on the shared channel key, not on individual sender identity. The MIC proves that the sender possesses the channel key, but any channel member can construct a valid packet with any claimed source address. This is a fundamental property of symmetric-key multicast—see Multicast Sender Authentication for further discussion.

A signed multicast (the SCF’s D bit set) is the exception. Its frame signature attributes the packet to the key that signed it, and is bound to the channel through the channel commitment, so it cannot be moved to another channel that shares its identifier. A multicast whose signature does not verify is no more attributable than an unsigned one.

Blind unicast payloads are additionally authenticated using pairwise keys derived from the sender and recipient’s key agreement, so only the true sender can produce a valid payload and only the intended recipient can verify it.

Joining a Channel

UMSH supports three models for channel membership, from simplest to most capable.

Named Channels

Channel keys may be derived from human-readable channel names rather than distributed as raw keys. A named channel is identified by a umsh:cs: URI (see URI Formats):

umsh:cs:Public

Named channels are effectively public—anyone who knows the name can derive the key and participate. Long, high-entropy names may provide practical obscurity, but this should not be treated as strong secrecy.

The channel key is derived from the channel name using HKDF-Extract:

channel_key = HKDF-Extract-SHA256(salt = "UMSH-CHANNEL-V1", ikm = canonical_name)

Where canonical_name is the canonicalized name portion of the umsh:cs: URI (everything after umsh:cs:). Canonicalization is defined as follows:

  1. Percent-decode the name portion of the URI.
  2. The decoded name MUST consist only of ASCII characters (letters, digits, and symbols). Non-ASCII (multi-byte UTF-8) names are not currently supported and MUST be rejected. Any character that is not URI-safe is carried through percent-encoding in the URI and recovered by step 1.
  3. Fold ASCII letters A–Z to lowercase a–z. No other characters are altered.
  4. Encode the result as an ASCII byte string; this is canonical_name.

For example, given umsh:cs:Public, the input to canonicalization is Public, which folds to public, and the key is derived over the bytes of public. Because folding is applied before derivation, umsh:cs:Public, umsh:cs:public, and umsh:cs:PUBLIC all derive the same channel key. The output is a 32-byte pseudorandom key that serves as the channel key. This key then flows through the standard Multicast Packet Keys derivation to produce K_enc and K_mic.

Note

Case-folding is restricted to ASCII deliberately. Correct case-folding of the full Unicode range is non-trivial (locale-dependent, with characters that fold to multiple code points), so UTF-8 channel names are deferred to a future revision. Note that percent-encoding does not provide a workaround: canonicalization operates on the decoded name, so a percent-encoded non-ASCII name still decodes to non-ASCII and is rejected. A group that wants a non-ASCII display name should use a private channel (umsh:ck:) with an explicit key and carry the display name as a URI parameter.

HKDF-Extract is appropriate here because named channels are not secrets—the name is public input keying material, not a password. Password-based KDFs (PBKDF2, Argon2) would add computational cost without meaningful security benefit, since the channel name is assumed to be known to all participants.

Private Channels

For channels that require real secrecy, the channel key is distributed out-of-band—via QR codes, umsh:ck: URIs (see URI Formats), or any other secure channel (including in-band exchange over an existing authenticated unicast session). Anyone who possesses the key is a member; there is no central authority and no mechanism to revoke membership without changing the key for everyone.

Managed Channels

A managed channel is administered by a designated managing node that controls membership. Unlike named and private channels, a managed channel supports adding and removing individual members without requiring all remaining members to re-join manually.

Note: The specific wire formats and MAC commands for managed channel operations (join requests, key distribution, rotation signalling) are not yet defined. The MAC layer itself is unaffected—managed channels use the same multicast packet format and cryptographic processing as any other channel.

To join a managed channel, a node provides its public key to the managing node—either out-of-band or via an in-band join request that the manager can accept or deny. Once accepted, the new member receives the current channel key and channel metadata from the managing node.

The managing node periodically rotates the channel key. When a key rotation occurs, each current member receives the new key along with the time at which it becomes active, allowing a coordinated switchover. Because the channel identifier is derived from the channel key, a key rotation also changes the channel’s on-wire identifier; the application layer masks this from the user so the channel appears to be the same.

To remove a member, the managing node distributes a new key to all members except the excluded node. The excluded node still holds the old key but cannot decrypt traffic encrypted under the new one.

Members that are offline during a key rotation can request the current key from the managing node when they reconnect.

Default Channels

Implementations should recognize two well-known named channels with specific behavior requirements.

public

The public channel (derived from umsh:cs:public) is the default flooded group chat channel. It provides a shared communication space analogous to an open town square—any node that knows the name can participate.

  • Maximum flood hops: 10.
  • Traffic may be encrypted (E=1), but the key is known so this doesn’t really offer privacy.
  • Blind unicast on this channel is forbidden.
  • Chat messages that do not include the full source key (S=1) MUST NOT be displayed in the user interface. This ensures that users can always verify sender identity on the public channel, even though the channel key itself is public knowledge.

EMERGENCY

The emergency channel (written EMERGENCY here for emphasis, but derived from umsh:cs:emergency after the ASCII case-folding described in Named Channels, so umsh:cs:EMERGENCY and umsh:cs:emergency derive the same key) is reserved for emergency communications. Repeaters should prioritize forwarding packets on this channel.

  • Maximum flood hops: 10.
  • Chat messages must not be encrypted—all emergency traffic must be readable by any node in range, including nodes that have not explicitly joined the channel.
  • Chat messages must include the full source key (S=1).
  • Every multicast carrying text-message traffic on this channel must be signed (the SCF’s D bit, see Frame Signatures). This includes resend responses to the channel and Message Unavailable responses. Resend requests and one-to-one repairs travel by blind unicast, which already authenticates the sender pairwise, and are not signed.
  • Messages that do not meet all three requirements (unencrypted, full source key, signed) must not be accepted or displayed by the user interface. A multicast whose signature does not verify does not meet them.

These requirements ensure that emergency traffic is universally readable, attributable to a specific node, and cryptographically authenticated against impersonation.

Text messages beginning with TEST: are test messages, which raise no alert by default.

Payload Reuse

Application-layer channel communication reuses the same payload types as unicast. For example, group chat uses the same text message and chat room payload formats as direct messaging. However, not all application types are valid over multicast—see Payload Types for compatibility.

Channel Access

This chapter describes how UMSH nodes contend for channel access before transmitting. These procedures apply to all transmissions—original packets, forwarded packets, and acknowledgments—unless otherwise specified.

Frame Duration

T_frame is the maximum on-air duration of a LoRa frame at the configured channel settings (spreading factor, bandwidth, coding rate, and maximum payload size). T_frame is not a fixed protocol constant; implementations derive it from the channel configuration. All timing parameters in this chapter are expressed as multiples of T_frame.

ULCP hosts can read PROP_PHY_T_FRAME from the device instead of calculating it from modulation-specific parameters.

For reference, typical T_frame values for a maximum-length (255-byte) packet using common MeshCore-style channel settings:

RegionSettingsT_frame
USA (915 MHz)BW 62.5 kHz, SF7, CR 4/5~0.8 s
Europe (868 MHz)BW 62.5 kHz, SF8, CR 4/8~2.2 s

Channel Sensing

In general, before transmitting any packet, a node SHOULD perform Channel Activity Detection (CAD), or whatever the appropriate analogous mechanism is for the given physical layer. CAD is a LoRa hardware primitive that detects preamble energy on the channel with minimal power draw.

  • If CAD indicates the channel is idle, proceed to transmit.
  • If CAD indicates the channel is busy, enter the backoff procedure.

Backoff Procedure

When CAD indicates the channel is busy:

  1. Wait a random duration uniformly sampled from [T_frame/40, T_frame/4].
  2. Perform CAD again.
  3. Repeat up to 15 more times (16 CAD attempts total).
  4. If the channel remains busy after all attempts, drop the packet silently.

Flood Forwarding Contention Window

When a repeater is eligible to flood-forward a packet, it SHOULD NOT just transmit like it would any other packet. Instead, it waits a contention delay proportional to the power of the received signal yet also inversely proportional to the quality of the received signal. Nodes that heard the packet cleanly but faintly transmit first; nodes that heard it very strongly wait longer; and nodes that heard it near the limit of demodulation wait until the others have had their chance. When a well-positioned repeater transmits, others overhear it, recognize the packet via duplicate suppression, and usually defer or abandon their own pending forwarding.

Note

This guidance is still provisional and should be treated as a starting point until it is validated with real-world measurements.

Although the contention parameters below are configurable in principle, nodes in the same mesh SHOULD use the same values so that forwarding behavior remains predictable. Unless a deployment intentionally overrides them, implementations SHOULD use the defaults in this section.

For the first forwarding decision after reception, compute the contention window as:

SNR_low = SNR_floor + 6 dB
SNR_high = SNR_floor + 18 dB
RSSI_low = -100 dBm
RSSI_high = -70 dBm

W_min = 0
W_max = T_frame/2
W_jitter = T_frame/10

quality = clamp((received_SNR − SNR_low) / (SNR_high − SNR_low), 0, 1)
signal = clamp((received_RSSI − RSSI_low) / (RSSI_high − RSSI_low), 0, 1)

if received_SNR < SNR_low:
    W   = W_max + W_jitter
else:
    W   = W_min + (W_max − W_min) × max((1 − quality), signal)
delay   = D_ack + W + uniform_random(0, W_jitter)

Where:

  • When flood-forwarding, the effective minimum SNR threshold is the higher of the Minimum SNR packet option (if present) and any locally configured minimum SNR. A repeater MUST NOT flood-forward if the received SNR is below that effective threshold. (Signal-quality thresholds do not apply to source-routed hops.)
  • SNR_floor is the lowest SNR at which LoRa demodulates at the spreading factor in use: −7.5 dB at SF7, and 2.5 dB lower for each step up, to −20 dB at SF12. The same reading is a comfortable margin at one spreading factor and a failing link at another, so the band is measured from the floor. Bandwidth moves the receiver’s sensitivity in dBm, not the SNR it needs.
  • SNR_low/SNR_high and RSSI_low/RSSI_high define the clamp ranges that normalize the two measurements for the contention heuristic.
  • A reception below SNR_low waits out the whole window and one jitter range before drawing its own jitter. Any repeater that heard the packet within the band transmits first, and this one forwards only if none did.
  • W_min is the minimum contention window for strong receptions.
  • W_max is the maximum intentional forwarding-delay window.
  • W_jitter bounds the random tie-breaking delay added after the deterministic window, so that nodes whose measurements agree do not transmit in the same instant.
  • received_SNR and received_RSSI are the SNR and RSSI measured during reception of the packet being forwarded.
  • D_ack is the ACK protection interval: a guard delay that applies when the forwarded packet may elicit an immediate ACK from its destination, and zero otherwise.

After computing the delay, the repeater waits. Other packets SHOULD continue to be forwarded while waiting, assuming the channel is clear.

If the repeater overhears the same packet forwarded by another node (identified by MIC in the duplicate cache) before the delay expires, it SHOULD defer rather than transmit. A safe default is to recalculate a new delay using the same W_min/W_max limits—including D_ack when it applies, since the overheard copy may itself elicit an immediate ACK from the destination—and restart the waiting period. The first two overheard copies each defer the pending forward this way; on overhearing a third copy, the repeater SHOULD abandon the pending forward.

Only a copy received over the radio defers. A packet received from an IP multicast group, heard by every bridge in the group at once, contends with a window drawn uniformly from [W_min, W_max].

If the repeater instead overhears a MAC ack whose ack_mic matches the pending packet’s MIC prefix, it SHOULD cancel the pending forward outright rather than defer: the destination provably has the packet.

This deferral behavior is intended only for the first local forwarding decision after reception. Once a repeater has actually transmitted its own copy, any later retransmission behavior is governed by Repeater Operation.

Nodes waiting for implicit forwarding confirmation MUST size their confirmation timeout to include this full forwarding-delay window. A safe default is to allow:

  • up to D_ack of ACK protection delay, when it applies
  • up to W_max + 2 × W_jitter of intentional forwarding delay
  • up to T_frame for the forwarded transmission itself
  • an additional guard margin of up to T_frame

ACK Protection Interval

The final destination of an ack-requested packet transmits its ACK as soon as the packet ends, without performing CAD (see Immediate ACK Transmission). CAD alone cannot protect that ACK from flood forwarders triggered by the end of the same reception: the contention delay W + uniform_random(0, W_jitter) may be arbitrarily small, CAD detects preamble energy and may miss an ACK already past its preamble, and a forwarder may not be able to hear the destination at all.

D_ack therefore provides deterministic separation. When the packet being flood-forwarded requests an ACK (UNAR or BUAR) and was received with no remaining source-route hops—the conditions under which its destination transmits an immediate ACK—the forwarder MUST delay by at least D_ack before transmitting, in addition to the computed contention delay. D_ack SHOULD cover the destination’s receive-to-transmit turnaround plus the on-air duration of a MAC Ack packet at the configured channel settings. The suggested default is 0.25 × T_frame; implementations that compute the actual MAC Ack airtime MAY use a tighter bound.

A packet received with source-route hops still pending does not elicit an immediate ACK from its destination, so D_ack does not apply when forwarding it.

Immediate ACK Transmission

When a node is the final destination of an ack-requested packet (UNAR or BUAR) and the packet has no remaining source route hops, the node SHOULD transmit the ACK immediately—without performing CAD—provided the radio is available for transmission. This is warranted because the channel is known to have been clear at the moment the received packet ended, and flood forwarders hold their transmissions back by the ACK protection interval so the ACK gets first use of the channel.

If the radio is not immediately available for transmission, the node SHOULD perform normal CAD and backoff before transmitting the ACK.

Repeater Operation

Forwarding logic is intentionally conservative. A repeater should evaluate packets in the following order.

Routing Invariants

The routing model is governed by a few simple rules:

  • Every currently defined on-mesh packet type is routable.
    • In the current protocol this includes broadcast, MAC ack, unicast, multicast, and blind unicast packets.
    • Reserved or opaque packet types are not routable until the protocol defines their forwarding semantics (there is only one at the moment, type 5).
  • A repeater forwards other nodes’ traffic, never its own and never traffic that has arrived.
    • A repeater MUST NOT forward a packet whose source address identifies one of its own identities, by hint or in full-key form. Re-flooding its own transmission wastes airtime, and prepending its record to the trace would fabricate a hop that never happened.
    • A repeater MUST NOT forward a packet whose destination address identifies one of its own identities. Such a packet has reached its destination; whether the repeater could actually process it is a separate question.
  • A packet is forwarded as either a source-routed hop or a flood hop, never both.
    • A hop named in the source route is a source-routed hop, including the hop that consumes the final hint.
    • Every other forwarded hop is a flood hop.
    • Rules written for flood forwarding—flood hop accounting, signal-quality thresholds, region policy, and forwarding contention—apply to flood hops only.
  • Repeaters MUST mutate specific dynamic routing metadata while forwarding (source route, trace, hop count, etc)
    • Typical examples are flood hop counts, traces, source routes.
    • A repeater SHALL NOT simply repeat a packet verbatim under any circumstances.
    • The Route Retry flag MUST NOT be added to a packet by anyone other than the original sender.
    • These mutations do not create a new logical packet.
  • A packet’s logical delivery identity and its repeater forwarding identity are related but distinct.
    • The final destination decides whether a packet is new by its normal replay and destination-processing rules.
    • Repeaters suppress duplicates using a forwarding identity that remains stable across legal forwarding rewrites.
  • Forwarding confirmation uses the same identity as repeater duplicate suppression.
    • This ensures a sender or repeater recognizes “the same packet, forwarded onward” even if the next hop mutates dynamic routing metadata.

Duplicate Suppression

Each repeater maintains a fixed-size cache of recently seen cache keys used to detect duplicate packets.

Parameters:

  • cache size = implementation configurable (see sizing guidance below)
  • eviction policy = oldest entry removed when full
  • entry lifetime = bounded (see entry expiry below)

The cache key is derived from the packet as follows:

  • Authenticated packets (unicast, multicast, blind unicast): the cache key is normally the packet’s MIC. Because the MIC covers all static fields and is unaffected by repeater modifications to dynamic options or the flood hop count, it remains stable across forwarding hops.
    • If the packet carries the Route Retry option, the cache key must distinguish that retry attempt from the same packet without the option present. A simple and sufficient rule is to treat the cache key as (MIC, route_retry_present).
    • This gives a packet two bounded forwarding identities: the original forwarding attempt and one explicit reroute attempt.
  • MAC acks and broadcasts: these packet types do not carry a MIC. The cache key is a locally-computed hash of the packet content, excluding the flood hop count and dynamic options—the same fields that would be excluded from a MIC. The hash does not need to be cryptographic; CRC-32 is suggested, but any hash with comparable distribution is acceptable. The choice of hash algorithm is a local implementation detail.

Before forwarding a packet, the repeater checks the cache:

  • if the cache key is already present, do not forward
  • if the cache key is not present, continue processing
  • once the repeater decides the packet is eligible, insert the cache key into the cache

To avoid racy reforward behavior, the repeater should insert the cache key into the cache as soon as it accepts the packet for forwarding, not after transmission completes.

Shorter cache keys increase the probability of false-positive collisions. Deployments that use 4-byte or 8-byte MICs should account for this when sizing the duplicate cache.

Entry Expiry

Cache entries MUST also age out. Capacity alone does not bound how long a key is suppressed, and a MIC-less packet’s cache key is derived from its content: a node that repeats an identical packet—a beacon, whose body is empty and whose non-dynamic options do not change—produces the same key every time. On a quiet mesh, capacity-only eviction would suppress that node’s packets for as long as the repeater runs.

An entry SHOULD be discarded once it is older than a cache lifetime measured from when the key was first inserted. One hour is a reasonable default: long enough that every retransmission of a single packet still collapses to one forward, short enough that a node re-announcing itself is heard again well within the time anyone would wait for it.

MAC-ack entries are the exception: their lifetime SHOULD be on the order of tens of seconds—the scale of a sender’s retry ladder—not an hour. An identical re-acknowledgement is the one duplicate a correct node emits deliberately (see the duplicate acknowledgement window), and it recovers a lost ack only if repeaters carry it; under a lifetime longer than the sender’s recovery horizon, the first forward of an ack absorbs every later one and the recovery path dies at the first hop. A short lifetime still collapses the copies of any single exchange, which play out within a few confirmation windows.

A repeat of a key already held MUST NOT extend that entry’s lifetime. Refreshing the timestamp on each sighting would let a node repeating itself inside the window hold its own suppression open indefinitely, which is the behavior expiry exists to prevent.

Cache Sizing

Each cache entry is small (equal to the cache key size—typically 4 to 16 bytes), so generous sizing is inexpensive. The recommended minimum is 32 entries; the suggested default is 64 entries. High-traffic deployments or networks with large diameters may benefit from 128 or more entries.

Forwarding Procedure

  1. Duplicate suppression

    • If this packet was forwarded recently, do not forward.
  2. Local origin and local destination

    • If this repeater’s own radio already transmitted this packet, do not forward. A repeater that shares its radio with another stack—an attached host, most commonly—receives a copy of what that stack transmits. Forwarding it would put a packet back on the air that this antenna already sent, and the source address belongs to the other stack, so the check below cannot catch it. The repeater MUST insert the cache key as if it had forwarded the packet: a neighbor’s repeat arrives shortly after, and it is the same packet, already carried.
    • If the packet’s source address identifies one of this repeater’s own identities, do not forward.
    • If the packet’s destination address identifies one of this repeater’s own identities, do not forward.
  3. Locally-handled unicast

    • If this packet was a unicast (blind or direct) packet that was fully handled and processed according to Packet Processing, do not forward. This covers the blind-unicast case, where the destination address is encrypted and step 2 cannot see it.
  4. Unknown forwarding-critical options

    • If the packet contains any forwarding-critical option the repeater does not understand, or whose value it cannot interpret, do not forward. Delivery-critical options do not affect forwarding.
  5. Policy checks

    • If the packet does not satisfy local repeater policy, do not forward.
  6. Source-route match

    • If the packet contains a non-empty source-route option:
      • If this repeater does not match the next source-route hint, do not forward.
      • Otherwise, remove the repeater’s own hint from the source-route option.
    • If the repeater mutates a source-route option, it MUST preserve the option on the forwarded packet even when no hints remain.
      • In that case, the forwarded packet carries a source-route option with zero remaining hops.
      • This preserves provenance: downstream nodes can still determine that the packet arrived via explicit source routing rather than by pure flooding.
    • If this repeater matched a source-route hint, it is forwarding a source-routed hop. Skip directly to step 10 (trace processing), including when the hint just removed was the last one. Steps 7 through 9 describe flood forwarding and MUST NOT be applied to a source-routed hop.
  7. Region policy (flood forwarding only!)

    • If the router has no region configuration at all, skip this section. A repeater with no configured regions applies no regional restriction and forwards a tagged packet whatever its region.
    • If none of the region codes in the packet match those configured on the repeater, do not forward.
    • If the packet has no region code option, the repeater SHOULD insert its configured default region before flood-forwarding. A repeater with no default region configured forwards the packet untagged; the default region is not implied by the configured region list.
      • If one or more region codes are already present, the repeater MUST preserve them unchanged.
      • A repeater MUST NOT add a second region code to a packet that already carries at least one region code.
  8. Flood hop accounting (flood forwarding only!)

    • If the packet has a flood hop count field with FHOPS_REM > 0, decrement FHOPS_REM and increment FHOPS_ACC.
    • Otherwise, do not forward.
  9. Signal-quality thresholds (flood forwarding only!)

    • If either the packet or repeater imposes a minimum RSSI, the effective threshold is the higher of the two. If the received RSSI is below the effective threshold, do not forward.
    • If either the packet or repeater imposes a minimum SNR, the effective threshold is the higher of the two. If the received SNR is below the effective threshold, do not forward.
    • These thresholds ask how well the packet was heard. A packet handed to the repeater over a point-to-point link rather than a radio was not heard at all, and carries no measurement to compare; the thresholds do not apply to it.
  10. Trace processing

  • If the packet contains a Trace option, add this repeater’s own entry, degrading the trace’s form as needed to fit the outgoing link (Trace Option Value). If even a lone 0xFF does not fit, do not forward the packet on that link, and count the drop.
  1. Retransmit
  • If this is a flood forward, implement the flood forwarding contention window
    • The contention window staggers the repeaters that all heard one transmission. A packet that arrived over a point-to-point link was heard by this repeater alone, so there is no contention to resolve and no window to wait out.
  • Forward the modified packet according to normal channel access rules.

A packet that arrives carrying an empty source-route option matched no hint at this repeater, so it takes the flood path: steps 7 through 9 apply in full. This is how a hybrid route transitions to flooding—the transition is observed by the repeater after the one that emptied the route, not performed by it.

A bridge rewrites nothing of its own: a repeater at each end of it forwards the packet by this procedure, so a crossing applies these rules twice.

A bridge in an IP multicast group applies this procedure once to a packet received from the group, as to one heard on its radio.

Forwarding Confirmation

Repeaters do not generate MAC acks—acks are generated only by the final destination. Instead, a node can passively confirm that a transmitted or forwarded packet was received by listening for a subsequent retransmission of the same packet (or it’s ack).

This applies to:

  • Source-routed packets: Each forwarding hop listens for the next hop—the node matching the next source-route hint—to retransmit.
  • Flood originators: The originating node listens for any node to retransmit.
  • Flood repeaters: Intermediate flood-forwarding nodes MUST NOT retry. Multiple nodes may forward the same flood packet, and a repeater has no designated next hop to listen for; retrying would increase congestion without improving reliability.
  • Routed MAC acks: An ack that carries a source route or flood budget is a routed send like any other; the destination that produced it listens for the first hop to carry it onward and retries on silence. Retrying the ack first is what spares the sender a full data retransmission when only the ack’s first hop failed. Once the destination sends a packet carrying an Ack MIC option for the same packet, it stops retrying an ack that has already been transmitted: repeaters drop their forwards of that ack on hearing the option, and the packet carrying it is confirmed and retried in its own right.

Confirmation, and the retry ladder below, apply whether or not the packet requests an ACK. An ack-requested sender goes on to await the ACK once forwarding is confirmed; a sender that requested no ACK is finished the moment it hears the packet carried onward, and if the retry budget runs out without that, the send simply ends—there is no failure signal to wait for. A point-to-point packet with no flood budget and no source route travels straight to its destination, confirms nothing, and MUST be transmitted exactly once.

After transmitting, the node listens for the same packet—identified by its cache key—to be retransmitted. This confirmation timeout MUST be large enough to cover the worst-case forwarding delay allowed by Channel Access, plus the airtime of the forwarded frame itself, plus a guard margin. A safe default is:

confirm_timeout = 2 × T_frame + W_max + 2 × W_jitter + D_ack

where W_max + 2 × W_jitter bounds the intentional forwarding delay permitted for the path, including the wait of a repeater that heard the packet below the contention band, and D_ack is the ACK protection interval when it applies. With the suggested defaults W_max = T_frame/2, W_jitter = T_frame/10, and D_ack = 0.25 × T_frame, this yields confirm_timeout = 2.95 × T_frame.

If the packet is heard before confirm_timeout expires, forwarding is confirmed. A copy received from an IP multicast group confirms as one heard on the air does.

If confirm_timeout expires without a retransmission, the node SHOULD schedule a retry after a jittered delay:

retry_delay = uniform_random(0, T_frame)

The delay does not grow with the retry number. Its only job is to decorrelate retries between nodes, and one frame time is enough for that; every additional window would be time the payload spends undelivered. After this delay expires, the retry is transmitted using normal CAD and backoff as described in Channel Access.

A node MUST NOT retry more than 3 times.

Ack Cancellation

A MAC ack echoes the acknowledged packet’s ack_mic—the first four bytes of its on-wire MIC—which any forwarder can read without keys, and which survives the mutations repeaters perform. A repeater that overhears a MAC ack (or an Ack MIC option) whose ack_mic matches the MIC prefix of a queued, not-yet-transmitted forward of an ack-eliciting packet (UNAR or BUAR) SHOULD cancel that forward: the destination provably has the packet, and repeating it spends airtime on nothing. The ACK protection interval puts the ack on the air ahead of pending forwards precisely so that this observation is available.

An overheard Ack MIC option also SHOULD cancel a queued, not-yet-transmitted forward of a MAC ack carrying the same ack_mic. The packet carrying the option delivers that acknowledgement to the same node, so the standalone ack has nothing left to deliver. An overheard MAC ack never cancels another ack’s forward: an ack’s trailer opens with the four bytes it echoes, so a second copy would cancel the first.

Cancellation acts on the queue, not on the future. It removes whatever matching forward is queued at that moment—a Route Retry copy included—and records nothing. A Route Retry copy received after a cancellation is a separate forwarding identity under duplicate suppression and is forwarded normally: the origin resorts to it precisely because the ack never reached it, and carrying the copy prompts the destination to acknowledge again. That copy is in turn cancelable by another overheard ack.

The duplicate-cache entry for a cancelled forward remains. The packet was handled; a later copy of the same attempt is still a duplicate.

A repeater holds no key to verify the ack_tag half of the trailer, so cancellation rests on the public ack_mic alone. The forgery this exposes suppresses at most one queued forward per overheard ack and is bounded by the Route Retry path.

Route Failure Recovery

When a node sends an ack-requested unicast or blind-unicast packet against a cached route and that attempt fails, it needs a way to re-attempt delivery without causing duplicate application delivery at the final destination.

Two kinds of cached route can fail this way, and they fail identically from the sender’s point of view:

  • an explicit source route, carried in the packet as a source-route option
  • a cached distance—the destination believed to be directly reachable, or reachable within a known number of flood hops—which narrows FHOPS and leaves no trace in the options

A practical recovery rule is:

  1. if the sender exhausts the retry budget for a packet sent against a cached route, it SHOULD treat that cached route as failed
  2. the failed route SHOULD be discarded or marked unusable for immediate reuse
  3. if the sender wishes to re-attempt delivery of the same logical packet, it SHOULD:
    • preserve the same frame counter, payload, and MIC
    • remove the stale source-route option, if one was present
    • add or refresh flood hops
    • include a Trace option if route rediscovery is desired
    • set the Route Retry option

These edits touch only fields the associated data excludes, which is what lets the MIC carry over. Adding FHOPS where the original had none also sets the FCF’s H bit, and the AAD clears that bit for this reason.

The restored flood radius SHOULD be the one the sending application asked for. A radius the application chose for itself is not a failed cache entry, and recovery MUST NOT widen it: a sender that was told to reach no further than one hop has not made a stale assumption, it has been given an instruction.

A sender that assumed nothing—it held no route and flooded at the whole budget it was allowed—has no cached route to discard, but its timeout is no more specific about which leg failed. The destination may hold a route back that no longer works, and its acknowledgment may have died on it. A MAC ack is never itself acknowledged, so the destination cannot discover this on its own. Such a sender SHOULD send one Route Retry of the packet when it has heard from the destination recently—within 10 minutes is a reasonable window—at the same flood budget and with a trace. A sender that has not heard from the destination recently SHOULD NOT: a destination that is unreachable answers a second full flood no more than the first.

Either way, a sender MUST NOT send a Route Retry of a packet that already carries the Route Retry option. A logical packet gets at most one.

Lost Acknowledgments

A Route Retry that arrives at the destination as a duplicate of a packet it has already acknowledged, after the duplicate acknowledgment holdoff, shows that the acknowledgment did not reach the sender: the sender sends a Route Retry only once its whole acknowledgment timeout has run out. The destination SHOULD then:

  • retire or demote the route its acknowledgment followed, and not relearn that same path from the retry for a while
  • re-acknowledge by a scoped flood rather than along any cached route: FHOPS_REM set to the retry’s FHOPS_ACC plus a one-hop margin, the retry’s region codes, and a trace, so that the sender learns a path back

A path the retry itself traced MAY still be adopted as an ordinary route candidate, but the re-acknowledgment does not follow it. A route the application has fixed in place MAY stay; the re-acknowledgment floods regardless.

Only an acknowledged original makes the retry evidence of a lost acknowledgment. A destination may have accepted the original without acknowledging it—overheard before its source route was consumed, for instance—and then the retry is the first copy it acknowledges, along its route as usual. A late duplicate without the Route Retry option may have crossed an acknowledgment still in transit, so it is re-acknowledged along the route without retiring it.

This recovery transmission is intentionally the same logical packet, not a new application message. The destination therefore still accepts it at most once according to the normal replay rules. The Route Retry option exists only to let repeaters forward the re-attempted packet even if they already suppressed the original as a duplicate.

This preserves a useful separation of responsibilities:

  • routing recovery remains a MAC concern
  • duplicate application delivery remains prevented by the end-to-end replay rules
  • repeaters remain largely stateless and do not need to understand application semantics

For flood-forwarding repeaters that have accepted a packet for forwarding but have not yet transmitted it, overhearing another forwarding of the same packet SHOULD normally cause a bounded deferral rather than an immediate transmission. A safe default is:

  1. on each of the first two overheard copies, recalculate a forwarding delay using the contention-window procedure in Channel Access
  2. restart the waiting period
  3. on overhearing a third copy, abandon the pending forward

This behavior is still provisional and should be validated empirically. The intent is to reduce near-simultaneous forwarding while still allowing a second or third repeater to contribute if an earlier forward was not widely heard.

Routing Implications

This forwarding model allows hybrid routing behavior.

For example, a packet can be source-routed to a specific repeater and also carry a flood hop count. The routed hops cost nothing against FHOPS_REM, so the whole budget is available to the flood that begins where the route ends. This permits “delivery-to-region, then flood” behavior, which is useful when searching for a node in a known geographic area without flooding the entire mesh, and it is why the flood radius limit of 15 bounds the flood rather than the total path length.

A sender sizing FHOPS_REM for a source-routed packet is therefore budgeting the flood beyond the route’s last hop, not the route itself.

Beacons & Path Discovery

Beacons

A Beacon is defined as either:

  • a broadcast packet with no payload, or
  • a multicast packet with no payload

Beacons are used to announce the presence of a node on the network without carrying additional data. Because beacons have no payload, they omit the 0xFF end-of-options marker; the options block is parsed until the end of the packet. The minimum beacon size is unchanged.

A beacon with a Trace option can inform listeners of both:

  • the node’s presence
  • a repeater path that may be usable to reach it

This is particularly useful when a receiver already knows the node’s identity information.

A trace that carries signal reports the quality of each hop as well as its identity, which is what distinguishes a path that merely works from one worth using.

Advertisements

An advertisement is a broadcast or multicast packet whose payload is a node identity payload—a beacon that additionally identifies and describes its sender. Advertisements are sent unsolicited, announcing presence, name, role, and capabilities. An advertisement carries a frame signature, which is what authenticates the identity it carries (see Signature Usage). To obtain a specific node’s identity, use the Identity Request MAC command, which is answered with a targeted unicast identity response rather than a broadcast advertisement.

Announcing on a Schedule

A node MAY emit beacons and advertisements on periods of its own. The two are configured separately because they announce different things at very different costs: a beacon publishes a path for a handful of bytes, while an advertisement carries an identity in a signed frame and is the largest frame a node originates unprompted. A mesh is normally best served by refreshing the path often and restating the identity rarely.

A scheduled advertisement SHOULD be sent without flood hops. What it carries is a standing statement rather than news, so flooding it across the mesh on every period spends airtime out of proportion to what a distant listener learns; a node that wants to be findable further away publishes a path with a beacon instead.

A configured period is a minimum rather than an exact cadence. A node SHOULD scatter each period by a random fraction of it—a quarter is a reasonable choice—and that scatter MUST only delay an announcement, never bring it forward, so the configured value remains a floor on how often the node transmits unasked. Nodes commissioned alike and powered on together otherwise stay in step for as long as they run, colliding every period and colliding again on each retry, since a shared schedule makes them contend from the same instant every time. Carrier sensing resolves the individual collision; the scatter is what keeps the mesh from having to resolve one on every period.

A node that has just restarted is the node whose neighbors hold the stalest paths to it, so emitting one beacon at bring-up is RECOMMENDED. That beacon is not delayed: nodes do not restart in unison, so bring-up is already scattered by whatever staggered it.

On a device managed over ULCP, these periods are the advertisement-policy properties.

Path Discovery

UMSH does not define a dedicated path-discovery packet type. Instead, path discovery is performed using existing primitives:

  1. Outbound discovery: Node A sends a unicast packet to Node B with the Trace option present and an appropriate flood hop count. The packet floods through the mesh; repeaters prepend their records to the Trace option as they forward.

  2. Path learning: When Node B receives the packet, the Trace option contains the sequence of repeaters traversed, ordered most-recent first. If the trace is complete, Node B can use its router hints directly as a candidate source route back to Node A.

  3. Return path: Node B can now send unicast packets to Node A using the learned source route. If the packet was ack-requested, Node B’s MAC ack also traverses the mesh, allowing Node A to confirm reachability.

  4. Bidirectional establishment: The trace Node A sent taught Node B a path back, and nothing else. Node B’s response—MAC ack, beacon, or application payload—teaches Node A a path only if it carries a trace of its own. Where the response is a MAC ack, that ack is the whole of what Node A receives.

When to Include a Trace

A beacon or advertisement that permits flooding SHOULD always carry a trace: publishing a path back to its sender is much of what it is for, and every listener it reaches may lack one.

A trace gives the destination a path back to the sender, so a sender spends one when the destination may not hold a working path back. A sender SHOULD include a trace on a unicast or blind unicast when any of the following holds:

  • it holds no route to the destination
  • the packet answers one from the destination that arrived with FHOPS_ACC greater than zero; a MAC ack is such an answer
  • no packet from the destination has arrived with FHOPS_ACC of zero in the last 5 minutes; a MAC ack correlated to the destination counts as a packet from it
  • it is a Route Retry resend

A packet that arrives with FHOPS_ACC of zero, off the sender’s own transmitter or along a source route, shows that its sender holds a working path to the receiver. That is what a trace would otherwise give it. A packet that had to flood to arrive shows the opposite. The 5-minute window bounds how long the evidence is trusted, since a path that worked can stop working.

A response to a packet that carried a trace MAY carry one. Any other packet MAY carry one, but a sender SHOULD otherwise prefer the smaller frame.

A packet carrying neither flood hops nor a source route SHOULD NOT carry a trace at all, whatever the sender knows. No repeater may forward such a packet, so the option can only arrive as empty as it left, and its arrival already proves what an empty trace would have said.

Because router hints are only two bytes, different repeaters may share the same hint, which may result in redundant (but harmless) forwarding along a source route.

Route Learning

A destination may overhear a frame while source-route hints remain unconsumed. The way back is then the remaining hints in reverse, followed by the trace: the whole path the sender chose, through the repeaters still to forward it. The trace alone would describe a shortcut past those repeaters, and a repeater being audible at the destination does not establish that it hears the destination. Historical buffered frames do not establish current reachability.

Copies of one packet, or of one MAC ack, that arrive over different paths each describe a path, and the first to arrive is not necessarily the best. A node MAY weigh them against each other and against the route it holds, using the per-hop signal of their traces where the copies carry it. A copy that arrives long after the first is a replay rather than evidence of a current path.

When a node successfully processes other live incoming packets, it can update its routing state for the sender:

  • Trace: if the packet contains a complete trace, with signal or hints only, the node caches its router hints as a source route for future packets back to the sender. Because the trace is accumulated most-recent first, it already describes the return path from the receiver back toward the original sender. This is the primary mechanism for learning precise multi-hop paths. A truncated trace is not a complete path, and no route is learned from it.

  • Flood hop count: if the packet contains a flood hop count, the node caches the sender’s FHOPS_ACC value together with any region-code options that arrived on the packet. When no source route is available, these cached flood parameters can be reused for flood responses—scoping the flood to approximately the right radius and regional domain rather than flooding the entire network.

  • Neither: a packet that arrives carrying no flood hop count and no source route was one that no repeater had permission to forward, so it reached the receiver off the sender’s own transmitter. The node SHOULD cache the sender as a direct neighbor. This is the same conclusion an empty trace supports, drawn from the packet’s structure rather than from an option, which is what lets an unforwardable packet leave the trace off.

A MAC ack can also supply routing evidence. It names no source, but its ack trailer correlates it to an outstanding request and so to the peer that sent it. Its trace can establish a route or replace a provisional direct observation with a repeater path. An ACK confirms that the exchange succeeded; its return path may differ from the outbound path, so it need not replace a working source route. Route selection and retention are local policy.

A packet that arrives carrying a source-route option—including one whose hints are all consumed—spends flood hops only after the route runs out, so its FHOPS_ACC counts the tail of the path rather than its length. Such a packet SHOULD NOT be used to derive a flood-distance estimate.

This routing state applies to all subsequent communication with the sender—replies, acknowledgments, and new messages alike. A node MAY replace a cached route when a newer packet provides a fresher trace, and SHOULD discard cached routes that have proven unreachable.

An ACK timeout is evidence that the exchange failed, not proof that the outbound leg alone failed. When an ACK-requested packet exhausts its retry budget on a cached route, the sender can return to route discovery:

  • discard or demote the cached source route
  • send the same logical packet again using flood hops instead of the stale source route
  • include a Trace option so that a fresh source route can be learned from the peer’s reply
  • set the Route Retry option so intermediate repeaters treat the rerouted attempt as a new forwarding opportunity even though the packet’s MIC and frame counter are unchanged

Trading a source route for flood hops rewrites only fields the associated data excludes. Adding FHOPS sets the FCF’s H bit, which the AAD clears, so the MIC carries over unchanged.

Recovery stays within the sender’s original flood ceiling; disabled flooding does not authorize a flooded retry. Once a replacement route is learned, normal routed transmission can resume. An ACK following Route Retry confirms delivery of the logical packet but does not identify which attempt delivered it.

A sender that held no route and flooded at its whole budget has no route to discard, yet the timeout may still be the destination’s fault: a route back to the sender that no longer works, on which the ACK died. When it has heard from the destination recently, the sender sends one Route Retry anyway, at the same budget and with a trace. A logical packet gets at most one Route Retry.

The destination learns from that retry. A Route Retry duplicating a packet it already acknowledged shows the ACK was lost, so the destination retires or demotes the route the ACK followed and re-acknowledges by a flood scoped to the retry’s FHOPS_ACC plus one hop, within the retry’s regions, carrying a trace. A path the retry traced may become an ordinary route candidate; the re-acknowledgment does not follow it. See Lost Acknowledgments.

Scoping Flood Hops to a Known Route

A wide flood hop count is a first-contact cost. Once routing state exists for a destination, the sender SHOULD scope FHOPS_REM to what the known path actually costs, plus a margin where the far end of the path is in doubt:

  • Source route: named repeaters consume hints without spending flood budget; the margin is available only after all hints are consumed.
  • Flood distance: the cached FHOPS_ACC is the radius at which the destination was last heard.
  • Direct observation: no repeater carried the observed packet.

The margin is one flood hop. It keeps delivery self-healing when the destination has moved since the path was learned, without paying for a mesh-wide flood on every packet. What decides it is the path’s last hop: the link between the destination and the first node that heard it. For a direct observation that node is the sender itself; for a traced path it is the earliest repeater in the trace, whose trace record holds how it heard the destination, when the trace still carries signal. A cached flood distance names no last hop. The sender SHOULD include the margin when:

  • the destination advertises itself as mobile (the MOB bit of its capabilities), unless it was observed directly and strongly;
  • the last hop was never measured; or
  • the last hop was measured weak.

The sender MAY omit the margin when the destination was observed directly and strongly or across a point-to-point link, when the last hop was measured without being weak, or when the application chose the path rather than learned it. Stronger than −75 dBm is a reasonable bar for a strong direct observation, and −96 dBm or weaker a reasonable bar for a weak last hop. A later packet whose path ends at the same last hop refreshes its measurement, whether or not that path replaces the one in use. A route that has failed outright is repaired through the route-retry behavior above, which floods at the sender’s full budget rather than the narrowed one.

A weak direct observation keeps the margin partly because the reverse direction may need a repeater the observed packet did not. ACKs can use the same margin, judged from the path the request taught, retaining the request’s region codes to scope any flooding even when the ACK also follows a source route. A strong link can still be one-way, so an ACK for a retransmission of a packet already acknowledged SHOULD carry the margin whatever its path: the retransmission shows the earlier ACK did not arrive. A consumed source route without a usable return trace supplies no total distance; a direct observation alone is insufficient to bound that ACK’s return path. The request’s flood ceiling governs the request, not the ACK’s independently selected return path.

An optional hop does not imply that a repeater is present. Not hearing a repeat is therefore insufficient reason to retransmit a directly addressed packet that requests no ACK, or an ACK sent directly with that allowance. A successful exchange with the margin does not show it was unnecessary.

Potential Improvement: Proactive Route Refresh

The recovery behavior above is reactive: a node continues using a cached source route until that route appears to have failed. In mobile scenarios, this may mean the sender does not attempt to discover a fresher route until after packets have already stopped flowing end-to-end.

One possible future improvement would be to allow a sender to occasionally perform a low-rate exploratory route refresh even when there is no strong indication of failure. This behavior is not part of the current specified protocol behavior and has not been validated with real-world measurements. It is described here only as a possible future optimization.

A conservative version of this idea would look like:

  • only perform exploratory refresh when the sender is believed to be mobile or moving
  • use a normal cached source route, but also include a Trace option
  • allow only a small flood budget, capped at one flood hop more than the source route has hints
  • perform this no more than occasionally, for example no more than once every N successful transmissions and no more than once every T minutes, whichever is longer

The intent would be to probe for a slightly better or fresher route without incurring the cost of a full rediscovery flood. A small tail flood could help discover alternate final hops or nearby replacement repeaters when the old route is only partially stale.

This approach has important limitations:

  • if the cached source route breaks early, a small tail flood will not help, because forwarding remains constrained by the explicit source route until that route is exhausted
  • excessive probing would waste airtime and increase contention, especially on busy meshes
  • a newly observed route is not necessarily better and may require local policy before replacing the old route

If this idea is ever standardized, meshes should converge on the same probing policy and parameter values so that behavior remains predictable across implementations. Any such policy should be treated as provisional until it has been evaluated on real radios in mobile conditions.

UMSH over IP Multicast

UMSH can be used over IP multicast the way it is used over LoRa. A frame sent to a multicast group on a local network is heard by every node in the group, as a frame transmitted on a LoRa channel is heard by every node in range, and the group carries UMSH as a LoRa channel does: nodes on it address one another, flood, route, acknowledge, and share channels by the same rules. Phones and computers on one network form a UMSH mesh among themselves, with no LoRa radio anywhere.

A node with a LoRa radio may join the group too. With its repeater enabled it is a bridge between the group and its LoRa channel, as described in Nodes With a LoRa Radio, so that the nodes on the network and the nodes within reach of its radio share one mesh. The group needs no LoRa radio to work.

Why a Multicast Group

A multicast group gives a local network an independent UMSH segment that works as soon as a node joins it. There is no server to set up and no connection to establish or authenticate: a node needs only the group’s address and port, which have defaults, and from then on it hears every other node in the group and is heard by them. Nothing at the transport needs authenticating, because UMSH secures its frames end to end and a node trusts a frame on its contents, never on where it arrived from.

The group can be this simple because it is the kind of medium UMSH is designed for. UMSH frames are sent to a medium, not to a peer:

  • A frame names no link-layer receiver. A unicast frame is addressed by a hint of its destination’s public key, and a multicast channel frame by its channel. It is transmitted once, every node in range hears it, and the node it is meant for recognizes it by its cryptographic contents.
  • A sender needs no picture of the network. A frame sent with no route reaches every node in direct range; beyond that, a flood carries it, and routes are learned from the traffic itself. Every node in the group is in direct range of every other, so a frame reaches its destination with no route at all.

Over unicast, the same segment would need each node to find its peers and send every frame once per peer, or a server for every node to connect to and relay through. Either is something to set up and keep running that the group does without.

A multicast group is appropriate only because UMSH is a low-bandwidth protocol. Its frames are at most 255 octets, and its rates are set by LoRa: well under a kilobyte per second. Every datagram sent to the group reaches every host in it and, on Wi-Fi, is sent to every station at a low basic rate. That cost is negligible at UMSH’s rates; a protocol that carried pictures or video would not belong in a multicast group, and UMSH is not designed to carry either.

Transport

Frames travel in UDP datagrams sent to an IPv4 multicast group. The default group is 239.255.134.121, in the administratively scoped range, and the default port is 15056. A deployment MAY configure another IPv4 multicast group and another nonzero port; every node that is to hear the others uses the same pair.

Every node sends from the group’s port as well as to it, and listens on it; several nodes on one host share it.

A node joins the group on the interface it uses for the local network and sends to the group from that interface, with a multicast TTL of 16 unless configured otherwise. How far the group reaches is the network’s decision: routers forward a multicast group only where they are configured to, and 239.255.0.0/16 is scoped to the organization. A network that routes the group between its subnets joins their nodes into one group.

Echoes

A node in a multicast group hears its own datagrams when multicast loopback, or an access point repeating a station’s multicast, hands them back. A node MAY drop a datagram whose source address and port are its own, which on a host with several nodes also drops the others’. A node that is alone on its host MAY instead disable multicast loopback.

Point-to-Point

A unicast address MAY be configured in place of the group, naming the one peer of a point-to-point link. The node joins nothing, sends to the peer at the group’s port, and drops every datagram whose source address and port are not the peer’s. Each end names the other, on the same port.

Datagram Format

A datagram carries exactly one frame, with no header: the frame exactly as it is transmitted over LoRa.

  • A sender MUST NOT send a datagram that is empty or larger than 255 octets.
  • A receiver drops, with accounting, a datagram that is empty or larger than 255 octets, and handles any other as a frame received over LoRa.

The sender of a frame is not identified in the datagram, and nothing about the datagram’s source address is carried onward.

A datagram is a group reception. It was heard by every node in the group, and measured by none of them: it carries no RSSI or SNR, and the signal-quality thresholds do not apply to it.

Traffic

A node sends a datagram only when it transmits a frame: one it originates, an acknowledgment, or, on a node with a LoRa radio, a frame its radio transmits. There is no keepalive, membership, or routing exchange of the group’s own.

Every node in the group hears every datagram, so one datagram delivers a frame to the whole group. A frame reaches the group again when a bridge repeats it onto LoRa, and what a radio sends to the group is bounded by its airtime and its duty cycle.

A node whose only medium is the group SHOULD NOT repeat. Every node it could reach has already heard the frame, so a repeat spends a flood hop and sends one more datagram to every host in the group while carrying the frame to no one new. A network on which a multicast datagram does not reach every node, such as an ad hoc Wi-Fi network whose nodes are not all in range of one another, is the exception: there a repeater carries frames to nodes the sender cannot reach.

Discovery

Nodes advertise themselves with DNS-SD over multicast DNS, under the service type _umsh._udp. A bridge SHOULD advertise; other nodes MAY.

  • The instance name is the node’s device name.
  • The SRV port is the group’s port.
  • The TXT record carries the keys below.
KeyValuePresent
vTXT schema version: 1Always
grpThe group address, dotted decimal with no leading zerosAlways
idThe node’s public key, as 44 base58 charactersAlways
fLoRa center frequency, in HzWhen the node has a LoRa radio with its PHY enabled
bwLoRa bandwidth, in HzWith f
sfLoRa spreading factorWith f
crLoRa coding rate denominator, 5 to 8With f
rep1 if the node repeats LoRa to LoRa, else 0With f
ib1 if an internet bridge link is upOptional

Numbers are decimal with no leading zeros. A node updates its record when any value changes.

A node reading a record ignores keys it does not recognize, treats a missing optional key as false, and ignores a record whose v it does not recognize. Discovery is only discovery: frames travel over the group whatever the records say, and a node learns from them which LoRa meshes the group reaches and on which settings, before it sends anything.

Nodes With a LoRa Radio

A node with a LoRa radio that joins the group is attached to both its LoRa channel, called the air below, and the group:

  • What arrives from the group is a reception. The node handles it as it handles a frame heard on its radio, recording that it arrived from the group, and applies the forwarding procedure to it once. With its repeater enabled the node may repeat it on its radio; with its repeater disabled it forwards nothing, whatever the frame arrived on.
  • What the radio transmits goes to the group. Each frame the node’s radio puts on the air is sent to the group, its repeats included. A frame the radio does not transmit—a repeat abandoned to a better-placed repeater, or canceled by an overheard acknowledgment—does not reach the group.

A node whose repeater is disabled is therefore a member of the group and nothing more; only a node with its repeater enabled is a bridge.

A frame a node on the group originates spends one flood hop at each bridge that carries it onto the air. A frame heard on the air and carried back onto the air by way of the group spends one hop at the bridge that hears it and repeats it, which is what reaches the group, and one at each bridge that receives it from the group and repeats it. A repeat returns to the group, where every node has already seen the frame and suppresses it as a duplicate.

A bridge that repeats a group reception prepends a Trace record with zero signal bytes, the record for a hop that measured nothing.

The duty cycle a radio spends repeating group receptions counts against its limit like any other forward. Copies sent to the group spend no radio airtime.

Ingress Limit

A bridge MUST bound the rate at which it accepts group receptions, so that a busy network cannot spend the radio’s airtime faster than its operator allows. A token bucket of frames per minute is recommended. Receptions over the limit are dropped with accounting.

Channel Access

Every bridge in the group receives each datagram at once, so bridges on the same LoRa channel would carry it onto the air together. Group receptions therefore contend, and only what is heard over the air says anything about the air.

  • Group receptions contend. A group reception carries no measurement to rank bridges by. Its flood contention window is drawn uniformly from [W_min, W_max], and the jitter and D_ack are added as for any other forward.
  • Only the air defers. Only a copy received over the radio defers or abandons a pending forward. A copy received from the group says nothing about who has transmitted on the radio’s channel, and is a duplicate and nothing more.
  • A group copy confirms. A copy received from the group is a frame some radio transmitted, and satisfies forwarding confirmation as a copy overheard on the air does.
  • Ack cancellation is unchanged. An overheard acknowledgment cancels a matching pending forward whatever it arrived on: it proves delivery either way.

Bridges on one LoRa channel that hear the same group reception draw different windows. The first to transmit is overheard by the others, which defer as they would for any repeat they overheard.

Security Considerations

  • Frames are protected end to end. A UMSH frame that carries security information is authenticated, and encrypted where it is marked encrypted, by the node that sends it, before it reaches any medium. The group adds no protection and removes none: what crosses the network is what crosses the air, and a node trusts a frame on its contents, never on where it arrived from.
  • Anyone on the network hears the group. A listener on the network learns what a LoRa receiver in range of the same nodes would.
  • Anyone on the network can send to the group. The group is as open as a LoRa channel is to anyone with a radio. A frame that fails authentication is dropped by the node it is addressed to. Where bridges are present, the ingress limit and the duty-cycle limit bound what injected traffic costs the air.
  • The group reaches as far as the network routes it. The default group is administratively scoped. A network that routes it between subnets has extended the group, and any bridges in it, to everyone the routing reaches.
  • DNS-SD records are public. A record reveals a node’s public key and, for a node with a LoRa radio, its LoRa settings—which its transmissions already reveal to anyone in range—to everyone on the network.
  • Bridges join meshes. The cautions in Routing Overview § Bridging apply: a bridge in the group puts the flood traffic of every LoRa mesh reachable through the group on its own mesh’s airtime. Region policy and the flood hop budget are the tools for keeping that local.

Payload Format

The UMSH payload carries higher-layer content—either a network-layer protocol (e.g., 6LoWPAN), a third-party application protocol (e.g., CoAP), or one of the UMSH-defined application protocols (e.g., text messages, chat rooms). The MAC layer treats the payload opaquely; it does not interpret, fragment, or reassemble payload content (see Layer Separation).

Payloads are typically prefixed by a 1-byte payload type identifier. Values from 128-255 (all values with the most significant bit set) are currently RESERVED.

Payload Type Registry

ValueMeaning
0Unspecified
1Node Identity
2MAC Command
3Text Message
4RESERVED
5Chat-Room Message
6RESERVED
7CoAP-over-UMSH
8Node Management Request
9Node Management Response
10Meet

Payload and Packet Type Compatibility

Not all payload types are valid with all packet types. A receiver should drop a packet whose payload type is not compatible with its packet type. For the purposes of this table, blind unicast follows the same rules as unicast.

Payload TypeUnicastMulticastBroadcast
Empty payloadYesYesYes
Node IdentityYesYesYes
MAC CommandYesNote 1Note 2
Text MessageYesYesNo
Chat-Room MessageYesNoNo
CoAP-over-UMSHYesYesNo
Node Management ReqYesNoNo
Node Management RespYesNoNo
MeetYesNoNote 3

Unless explicitly configured otherwise, the only payload types allowed for broadcast are empty payloads, node identities, the broadcast-permitted MAC commands (Note 2), and Meet Hellos (Note 3).

Note 1: Some MAC commands may be permitted on specific channels. For example, a private channel might allow echo requests to all members and receive echo responses from everyone. Whether a given MAC command is accepted over multicast is deployment-defined and not yet specified by the protocol.

Note 2: MAC commands are admitted to broadcast individually: a command may be carried in a broadcast only when its definition says so, and any such definition must weigh the solicitation load a broadcast can create. Currently only the Identity Request permits broadcast carriage, under the flood-management restrictions defined there. Receivers drop any other MAC command arriving by broadcast.

Note 3: Only a Hello rides a broadcast. Every other Meet kind is unicast between session keys.

Response Carriage

A response to a unicast request is carried the same way the request was: a request that arrived as blind unicast on a channel is answered as blind unicast on that same channel, and a request that arrived as plain unicast is answered as plain unicast. This holds for every request and response the protocol defines—MAC commands, node identity, node management—and applies whether the response comes from the MAC layer or from an application above it.

Blind unicast conceals both endpoints from observers who lack the channel key. A response sent off the channel names the parties the request took care to hide, so it would undo that concealment for the exchange as a whole. A MAC acknowledgement is not a response and remains channel-less; it carries no destination hint and so names neither party.

A node answering a multicast or broadcast request is not bound by this rule, since such a request has no pairwise carriage to mirror. What a node may answer with is left to each command’s own definition.

In-Band Node Management

Nodes may optionally support remote management via Node Management Request and Node Management Response payloads, which carry ULCP exchanges over the mesh itself. The payload format, authorization model, and reachable state are specified in Node Management; support is advertised through the CAP_ADMIN ULCP capability.

Node Identity

The node identity payload is an application-layer structure carried inside the UMSH payload. Its contents—including the timestamp option below—are not interpreted or required by the MAC layer. The MAC layer itself is timestamp-free (see Frame Counter).

Structure

+------+------+---------+------+-----------+
| ROLE | CAPS | OPTIONS | 0xFF | SIGNATURE |
+------+------+---------+------+-----------+
  1 B    1 B   variable   1 B     64 B

Fields:

  • ROLE (1 byte)—the node’s primary role.
  • CAPS (1 byte)—the node’s capability bitmap.
  • OPTIONS (variable, optional)—a CoAP-style option list of node identity options, using the same delta-length encoding as packet options.
  • 0xFF (1 byte, optional)—options-terminator marker. Present only when a signature follows.
  • SIGNATURE (64 bytes, optional)—EdDSA signature covering ROLE through the 0xFF terminator, inclusive.

The smallest node identity payload is two bytes: role and capability bitmap, with no options or signature.

Node Primary Role

Defined values:

  • 0—Unspecified
  • 1—Repeater
  • 2—Chat
  • 3—Tracker
  • 4—Sensor
  • 5—Bridge
  • 6—Chat Room
  • 7—Temporary Session
  • all other values—Reserved

Capability Bitmap

A single byte describing optional feature support, orthogonal to the primary role:

  7   6   5   4   3   2   1   0
+---+---+---+---+---+---+---+---+
| - | - |CoA|CHR|TLM|TXT|MOB|REP|
+---+---+---+---+---+---+---+---+
  • bit 0 (REP)—Repeater
  • bit 1 (MOB)—Mobile/Handheld (As opposed to “fixed”)
  • bit 2 (TXT)—Text Messages
  • bit 3 (TLM)—Public Telemetry
  • bit 4 (CHR)—Chat Room
  • bit 5 (CoA)—CoAP
  • bits 6–7—RESERVED (set to zero; ignore on read)

A node may advertise multiple capabilities independently of its primary role. For example, a node with role Chat may also set the REP bit to advertise repeater duty.

Node Identity Options

Options use the CoAP-style delta-length encoding defined in Packet Options.

NumberNameValue
0Node NameUTF-8 string
1Node Location1-7 bytes, see Variable-Precision Location Format
2Altitude in Meterssigned integer, meters above mean sea level
3Unix Timestampunsigned integer, seconds since the Unix epoch, UTC
4Supported Flood Regionsone option per region, its UTF-8 string form, max 24 bytes each
5Nonce4 bytes, echoed from a soliciting Identity Request

Node Name (option 0)

A UTF-8 display name for the node, typically shown in user interfaces. Max length: 24 bytes.

Node Location (option 1)

The node’s geographic position, encoded as a variable-precision grid code. See Variable-Precision Location Format. Max precision: 7 bytes. Implementations MUST ignore bytes beyond the seventh and MUST NOT encode more than 7 bytes.

Altitude in Meters (option 2)

The node’s altitude above mean sea level in meters, encoded as a minimal big-endian signed integer (leading 0x00 and 0xFF sign-extension bytes omitted, provided the sign bit of the remaining value is unambiguous). Max length: 4 bytes.

Unix Timestamp (option 3)

Seconds since the Unix epoch indicating when this identity payload was generated. Lets a consumer judge how fresh the identity is—most useful when the identity stands alone (e.g. in a QR code), where a stale capture could otherwise be presented indefinitely. Not used by the MAC layer. Encoded as a minimal big-endian unsigned integer (leading zero bytes omitted). Max length: 4 bytes.

Supported Regions (option 4)

For repeaters, the regions the node will flood-forward for, one option per region. Each entry carries the region’s string form—the short code for a region that has one, the region name otherwise—rather than its two-byte region code: the code is always derivable from the string, while a hash-derived code cannot be turned back into a name. A sender lists at most 10 regions, and the identity payload must still fit its enclosing packet, so a node whose full list does not fit omits regions; the option names regions the node forwards for without promising to name every one. A node that omits the option entirely makes no claim about its regional forwarding policy. Entries carry the capitalization the region was written with, and a receiver comparing them derives their codes, which ignores ASCII case. Max length: 24 bytes per entry, the bound on region names.

Nonce (option 5)

Copied verbatim from the Identity Request that solicited this identity payload, letting the requester correlate the response to its request. Present only in responses whose request carried a NONCE option. Length: 4 bytes.

Variable-Precision Location Format

The node location is encoded as a string of one or more bytes, where each byte narrows the position to a 16×16 sub-grid of the preceding byte’s cell. Additional bytes increase precision; trailing bytes may be omitted to give a coarser—and therefore more privacy-preserving—location.

Grid Subdivision

Each byte splits its parent cell into a 16×16 grid of children. Within each byte, the high nibble indexes along latitude and the low nibble indexes along longitude:

  7   6   5   4   3   2   1   0
+---------------+---------------+
|   LAT NIBBLE  |   LON NIBBLE  |
+---------------+---------------+
     4 bits          4 bits

The first byte subdivides the entire globe (latitude in 16 slices of 11.25°, longitude in 16 slices of 22.5°). Each subsequent byte subdivides the cell selected by the byte before it, using the same high-nibble-latitude, low-nibble-longitude convention.

Latitude leads here for the same reason it leads everywhere else in UMSH: a coordinate pair is written, spoken, and passed as (latitude, longitude), and an encoding that reversed the pair would be the one place the convention did not hold.

Encoding a Location

Given latitude LAT in degrees (-90..+90) and longitude LON in degrees (-180..+180), an N-byte code can be derived either in a single step or byte by byte. The direct form is normative; where floating-point rounding makes the iterative form disagree at the final nibble, the direct form’s result is the correct code.

Direct form

Compute two 4N-bit indices over the full desired precision:

  • lat_index = floor((LAT + 90) × 16^N / 180)
  • lon_index = floor((LON + 180) × 16^N / 360)

Then read nibbles from most significant to least significant:

  • byte k high nibble = (lat_index >> (4 × (N − 1 − k))) & 0xF
  • byte k low nibble = (lon_index >> (4 × (N − 1 − k))) & 0xF

This form makes the hierarchy explicit: truncating an N-byte code to k bytes yields exactly the k-byte code for the same position.

Edge cases: LON = +180° is equivalent to LON = −180° and wraps lon_index to 0. LAT = +90° is a single degenerate point; clamp lat_index to 16^N − 1.

Iterative form

Emitting one byte at a time, with lat_step = 11.25° / 16^k and lon_step = 22.5° / 16^k:

  • byte k high nibble = floor(((LAT + 90) mod (16 × lat_step)) / lat_step)
  • byte k low nibble = floor(((LON + 180) mod (16 × lon_step)) / lon_step)

For byte 0 (k = 0), lat_step = 11.25° and lon_step = 22.5°, so the modulus is a no-op for valid inputs and the formulas reduce to:

  • high_nibble = floor((LAT + 90) / 11.25)
  • low_nibble = floor((LON + 180) / 22.5)

The same edge cases apply as in the direct form: LON = +180° wraps to nibble 0 naturally via the modulus, but LAT = +90° must be clamped to nibble 0xF at every position—the modulus would otherwise wrap it to nibble 0.

Worked Example

Encode (LAT, LON) = (37.331°, −121.883°) (San Jose, CA) at 3-byte precision.

Direct form:

  • lat_index = floor(( 37.331 + 90) × 4096 / 180) = floor(2897.49) = 2897 = 0xB51
  • lon_index = floor((−121.883 + 180) × 4096 / 360) = floor(661.24) = 661 = 0x295

Reading nibbles most-significant first:

ByteHigh (lat)Low (lon)Value
00xB0x20xB2
10x50x90x59
20x10x50x15

Final code: B2 59 15.

Decoding a Location

An N-byte code denotes the entire cell it selects, not a point. When a single coordinate is needed (e.g. to plot on a map), decoders use the center of the cell, with an uncertainty of ± half a cell in each axis. Using any other point (such as the cell’s south-west corner) would place decoded positions up to half a cell apart between implementations.

Precision Scaling

Each additional byte divides both the latitude and longitude spans by 16. The span shrinks geometrically, so just a few bytes yield very fine precision:

BytesLatitude cellLongitude cellEquator cell size (approx.)
111.25°22.5°1,250 × 2,500 km
20.703125°1.40625°78 × 156 km
30.0439°0.0879°4.9 × 9.8 km
40.00275°0.00549°305 × 610 m
50.000172°0.000343°19 × 38 m
61.07 × 10⁻⁵°2.15 × 10⁻⁵°1.2 × 2.4 m
76.71 × 10⁻⁷°1.34 × 10⁻⁶°7.5 × 15 cm

Longitude cells narrow with latitude, so cells are physically smaller in east-west extent away from the equator.

Comparison with float32: Two single-precision floats (8 bytes) give non-uniform resolution: ~85 cm latitude and ~1.7 m longitude worst-case near ±90°/±180°, improving to ~1 cm near 0°. At 7 bytes, this encoding achieves ~7.5 × 15 cm uniformly across the globe—better than the float32 worst case while using one fewer byte. At 8 bytes—beyond the 7-byte wire limit, considered here only for an apples-to-apples comparison against the 8 bytes two floats occupy—the cell shrinks to ~5 × 9 mm, better than float32 everywhere.

Properties

  • Simple encoding. Two nibble divisions per byte; comparison and truncation are pure integer operations, and decoding needs only integer or fixed-point arithmetic.
  • Arbitrary precision. Any desired accuracy is reachable by adding bytes.
  • Compact. Scales linearly with precision: one byte per factor-of-16 refinement in both axes.
  • Free coarsening. Reducing precision is just truncation; no recomputation is needed. This makes it trivial to publish, say, a 2-byte location in a broadcast and a 5-byte location in a private message, both derived from the same underlying position.

Caveats

  • Prefix locality is one-way. Codes sharing a prefix select nearby cells, but nearby positions straddling a cell boundary may share no prefix at all. Prefix comparison alone is therefore suitable only for coarse filtering; proximity queries must also check neighboring cells.
  • Coarseness is angular, not metric. Because longitude cells narrow with latitude, a given byte count discloses a physically smaller area at high latitudes. When truncating for privacy, choose the precision by the physical extent of the resulting cell rather than by byte count alone.

Location Privacy

This section is non-normative implementation guidance for senders, with one exception noted below. Receivers cannot distinguish a diluted position from a true one, so nothing here affects the wire format or interoperability.

Truncation is the first privacy tool: dropping bytes discloses only a cell. But a cell is a set, and an observer with context can shrink it. If a cell is mostly water and the tip of an isthmus barely pokes into it, reporting that cell effectively reports the isthmus tip, no matter how large the cell is. Truncation alone cannot defend against such priors, because the disclosed region is always aligned to the fixed cell lattice.

The defense is to add a deliberate position offset before encoding, so that the feasible region becomes the reported cell dilated by the offset’s magnitude—spilling across cell boundaries and decoupling the disclosure from the lattice. Done naively, however, this mechanism leaks more than plain truncation. The recommended construction and the pitfalls it avoids follow.

  1. Offset in meters, not degrees. Draw a planar offset (east and north components in meters) and convert to degrees at the current latitude. An offset specified in degrees gives latitude-dependent, anisotropic protection.
  2. Uniform distribution. Draw the offset uniformly, over either a disk of radius R (isotropic) or a box of half-width R (simplest: two independent uniform components taken directly from a keyed hash). A uniform offset makes every position within R of the report equally plausible. A peaked distribution such as a Gaussian defeats the purpose: its density gradient leaves the reported position the single most probable true position, so the report still points at the sender, just fuzzily. The cost of bounded support is that an observer knows the true position is certainly within R of the report—but every bounded distribution shares this, and unbounded tails trade it for occasionally reporting positions an absurd distance away.
  3. Magnitude matched to the published precision. Choose R between roughly 0.5× and 2× the extent of the cell at the coarsest precision being protected. Much below that range the reported cell almost never differs from the true one and the offset is a placebo; much above it the reports are useless. Note that the offset and truncation are complementary: truncation coarsens in 16× steps, while R tunes ambiguity continuously between those steps.
  4. Deterministic per place. Derive the offset from a keyed hash of a secret location-privacy key and the true position quantized to a coarse derivation cell (comparable in extent to R). The same place then always yields the same offset—across reports, reboots, and revisits—with no random-number state to persist and no re-draw event to observe or provoke.
  5. Hysteresis at derivation boundaries. A device straddling a derivation-cell boundary must not flap between the two derived offsets: keep the current offset until the true position moves well inside a neighboring derivation cell. Flapping hands an observer two independent samples of nearly the same position, and the flapping pattern itself localizes the device to the boundary.
  6. One diluted position feeds all encodings. Apply the offset once, to the underlying position, and derive every published precision by truncating that single result. If a coarse broadcast and a fine private message are diluted independently, comparing them yields two samples of the same position.
The Resampling Trap

The offset MUST NOT be re-drawn per report. This is the one normative statement in this section, because the failure is worse than doing nothing: fresh noise per report combined with quantization is a dithering scheme. The expected value of the reported cell is a continuous, monotone function of the true position, so an observer averaging repeated reports recovers the position to a precision limited only by the number of samples—below the cell size, without bound. A stationary node adding fresh noise to every identity broadcast discloses more over time than one publishing its true cell.

The intuition that quantization backstops the noise is exactly backwards: deterministic truncation has a hard resolution floor; truncation of freshly-noised input has a soft floor that averages away.

Limits
  • Mobile nodes. A fixed offset protects a stationary position well. If the device moves while the offset is held, the reported track is the true track translated by a constant vector, and matching the track’s shape against roads or coastlines recovers the offset exactly. For mobile nodes the mechanism obscures where a track is anchored, not its shape; treat it accordingly.
  • Ground-truth correlation burns the offset. Any single correlation between a reported position and the true one—a precise disclosure through another channel, a physical encounter—reveals the offset for as long as it is held. After such an event the privacy key (or derivation input) should be rotated.
  • Radio-layer localization is out of scope. The mechanism launders only the advertised location field. Observers in RF range can localize a transmitter by which nodes hear it and at what signal strength, regardless of what its identity payload claims. The mechanism is meaningful against remote consumers of identity payloads, not against nearby receivers.

Signature Usage

The optional 64-byte EdDSA signature lets an identity stand on its own, apart from any frame, as in QR codes and exported identities.

An identity carried in a frame is authenticated by that frame instead, and need not carry the embedded signature. An advertisement is authenticated by its frame signature, and an identity response by its unicast MIC.

A receiver believes an identity only when one of these holds:

  • its embedded signature verifies against the sender’s key;
  • it has no embedded signature, and its frame authenticates the sender: a verified frame signature, or a unicast or blind unicast MIC.

A multicast MIC alone does not authenticate the sender, since any channel member can produce one under any source address. An identity whose embedded signature does not verify is not believed.

A frame signature is checkable only against the sender’s public key. An advertisement meant for receivers that may not yet hold that key MUST therefore carry its source address in full-key form; a hint-only advertisement can be verified only by a receiver that already holds the key.

An identity authenticated by its frame SHOULD replace stored details only when that frame advanced the sender’s established receive baseline (see Replay Detection). A frame accepted as first contact, or one accepted from behind the baseline, MAY fill details that are missing but SHOULD NOT replace stored ones: a late frame can carry details older than those already stored. A baseline restored from storage is established.

Contact Exchange

![NOTE] This section is an early work in progress and this protocol may change significantly.

Meet payloads (payload type 10) let two people standing together exchange contacts without typing an address. Each device announces itself under a session key drawn for the purpose, showing only its model and a color derived from that key. One holder asks, the other agrees, and both screens show a six-digit code. Each holder compares the codes and presses Same or Different. A device sends its own long-term key and node identity only when its holder presses Same.

Nothing a device sends before its holder’s Same identifies it beyond its model. The person standing there is the identification, and the code confirms that the two devices talking are the two in their hands.

Session Keys

A session key is a fresh Ed25519 key pair, used as an ordinary node identity for the length of one session and never again. Every Meet frame is sent from the session key with the full source key in the header, and every unicast is addressed to the other device’s session key.

A device MUST draw a new session key, and new nonces, for every exchange attempt. That includes trying again after an attempt ends in Not now, Busy, Different, a timeout, or a failure. When an attempt ends for any reason, the session key’s private half is erased.

A session key answers for nothing but Meet. A device MUST NOT accept or answer any other payload addressed to a session key: no node identity, no MAC command, no text. Meet unicasts addressed to a long-term identity are dropped.

Payload Format

+------+---------+------+------+
| 0x0A | VERSION | KIND | BODY |
+------+---------+------+------+
  1 B     1 B      1 B    var

VERSION is 1. Any change to a body layout, or a new kind, takes a new version. Every kind except Identity has a body of exactly the length given below. Identity’s body runs to the end of the payload. No kind is followed by anything: bytes past the end of a body make the payload malformed.

KindNameCarriageBody
0HellobroadcastMODEL (2 B)
1RequestunicastCOMMITMENT (32 B)
2AcceptunicastNONCE (16 B)
3RevealunicastNONCE (16 B)
4IdentityunicastKEY (32 B), node identity payload
5EndunicastREASON (1 B)

A Hello is carried in a broadcast with no flood hops. Every other kind is carried in an encrypted unicast between the two session keys, with an acknowledgment requested and no flood hops. A kind on the wrong carriage is malformed.

Hello

MODEL is the sender’s hardware model, a model identifier (big-endian, 0 when unspecified). A Hello carries nothing else. It lists the sender on the receiver’s Nearby screen, keyed by its session key.

Request

Sent by the device whose holder asked (A) to the device asked (B):

COMMITMENT = SHA-256("UMSH meet commit" ‖ S_A ‖ S_B ‖ N_A)

S_A and S_B are the two session public keys, and N_A is 16 random bytes A keeps until Reveal.

Accept

Sent by B when its holder agrees. NONCE is N_B, 16 random bytes.

Reveal

Sent by A on receiving Accept. NONCE is N_A. B checks that the commitment it holds equals SHA-256("UMSH meet commit" ‖ S_A ‖ S_B ‖ N_A). On a mismatch B sends End with reason failed.

Identity

+-----+---------------------------------------------+
| KEY | ROLE | CAPS | OPTIONS | [0xFF | SIGNATURE] |
+-----+---------------------------------------------+
 32 B   node identity payload, to the end of the frame

KEY is the sender’s long-term public key. The rest of the body is its node identity payload, read as follows:

  • ROLE, CAPS, then options until the end of the frame or a 0xFF marker.
  • If the end of the frame is reached, the payload is unsigned.
  • If a 0xFF marker is found, exactly 64 bytes MUST follow it, and they are the signature. Any other count, including none, makes the Identity malformed.

The signature is the ordinary node identity signature: Ed25519 by KEY over ROLE through the 0xFF marker, as in a node URI bundle. It is optional. A signature that does not verify under KEY makes the Identity malformed.

An Identity carries no session binding. A sender that passed along someone else’s key and payload could neither read what is sent to that key nor send as it.

A device sends its Identity when its holder presses Same, and the Identity stands for that Same.

End

Ends the attempt for both devices.

ReasonMeaning
0Not now: the asked holder declined
1Busy: the receiver is in another exchange
2Different: a holder pressed Different
3Left: a holder backed out or closed Add peer
4Timed out
5Failed: a frame inside the exchange was malformed or did not check out

An unknown reason still ends the attempt.

Code and Color

Both devices show:

CODE = BE32(SHA-256("UMSH meet code" ‖ S_A ‖ S_B ‖ N_A ‖ N_B)[0..4]) mod 1000000

as six decimal digits, zero-padded.

Each session key has a color. Its hue step is:

HUE = SHA-256("UMSH meet color" ‖ S)[0]

The color is the OKLCH color with lightness 0.75, chroma 0.12, and hue angle HUE × 360° / 256, converted to 8-bit sRGB:

  1. a = 0.12·cos(h), b = 0.12·sin(h).
  2. l' = 0.75 + 0.3963377774·a + 0.2158037573·b m' = 0.75 − 0.1055613458·a − 0.0638541728·b s' = 0.75 − 0.0894841775·a − 1.2914855480·b
  3. l = l'³, m = m'³, s = s'³.
  4. Linear sRGB: R = 4.0767416621·l − 3.3077115913·m + 0.2309699292·s G = −1.2684380046·l + 2.6097574011·m − 0.3413193965·s B = −0.0041960863·l − 0.7034186147·m + 1.7076147010·s
  5. Each channel x, clamped to [0, 1], is encoded as 12.92·x when x ≤ 0.0031308 and 1.055·x^(1/2.4) − 0.055 otherwise, then scaled by 255 and rounded.

All 256 colors fall inside the sRGB gamut. Implementations may precompute them as a table.

Exchange

  1. Both devices broadcast Hellos and list each other.
  2. A’s holder picks B’s row. A sends Request.
  3. B’s holder is asked. Agreeing sends Accept. Declining sends End (not now).
  4. A sends Reveal, and shows the code.
  5. B checks the commitment and shows the code.
  6. Each holder compares. Same sends that device’s Identity. Different sends End (different).
  7. A device that has both sent and received an Identity has exchanged.

The commitment is sent before either nonce, and each nonce only after the other side is bound: B sends N_B only while holding C_A, and A reveals N_A only after receiving N_B.

What a device does with a received Identity is up to it, except that it MUST discard it if its own holder presses Different or leaves.

Rules

  • An Identity received before the device shows the code is dropped.
  • An Identity whose KEY is the receiver’s own long-term key is dropped.
  • When both devices send Request to each other, the device with the lower session key (compared as big-endian unsigned integers) ignores the incoming one. The other drops its own Request and sends Accept without asking its holder, who already asked.
  • A Request arriving while the receiver is in an exchange, or after its session’s attempt has ended, is answered with End (busy).
  • A device sends no Hellos while an attempt is in progress.

Timeouts

Waiting forLimit
Accept, after Request60 s
The asked holder’s answer60 s
Reveal, after Accept10 s
A holder’s Same or Different, after the code is shown2 min
The other Identity, after sending one’s own2 min

A device whose limit passes sends End (timed out).

Malformed Frames

A frame from the session key a device is in an exchange with, by unicast, is inside the exchange. Inside the exchange, any malformed frame—an unknown version or kind, a wrong length, a wrong carriage, or an Identity that does not parse or verify—ends the attempt: the receiver sends End (failed) and keeps nothing.

Any other frame is outside. A malformed frame outside an exchange is dropped and never answered. A device speaking a newer version is simply not listed.

A well-formed frame that does not fit the current state is dropped.

Nearby

A device sends a Hello when it opens a session, one more between 0.5 and 2 seconds after it first hears a session key it has not listed, and then periodically. The periodic interval is at least 5 seconds, scaled to the airtime of the current channel settings, and each one is jittered by up to a quarter either way. A row ages out after three intervals without a Hello.

Two devices of the same model whose hue steps are within 24 of each other (about 34°, wrapping around) are hard to tell apart. While idle, the device with the lower session key of such a pair opens a new session, with a new key and a new color.

Transmit Power

A device SHOULD send every Meet frame, and every acknowledgment of one, at −9 dBm, or at the least power its radio can transmit if that is higher. The two holders stand together; nothing farther away needs to hear them.

Linking a Name to a Key

A Meet Identity travels between session keys, not from KEY, so nothing but its signature links its payload to KEY. An unsigned Identity’s name and other claims are the sender’s word only.

MAC Commands

A MAC command payload consists of:

  • 1 byte: command identifier
  • optional bytes: command-specific payload

Support for MAC commands is optional.

A receiver reads the fields a command defines and ignores any bytes that follow them. A command shorter than its defined fields, or with an unrecognized identifier, is ignored.

MAC commands are addressed to a single node. Unless a command’s definition provides rules for multicast or broadcast use, as Identity Request does, a node ignores a command that arrives by multicast or broadcast.

A node answering a command carries its response the way the request was carried, as Response Carriage defines.

Command Registry

ValueCommandDirection
0UNALLOCATED–
1Identity RequestRequest
2Signal Report RequestRequest
3Signal Report ResponseResponse
4Echo RequestRequest
5Echo ResponseResponse
6PFS Session RequestRequest
7PFS Session ResponseResponse
8End PFS SessionEither
9No-opRequest
10Peer Repeaters RequestRequest
11Peer Repeaters ResponseResponse
12Verify FrameRequest
13Frame VerifiedResponse

Identity Request (1)

Requests that the destination respond with its node identity.

A common use is resolving a node hint to a full address. A node that knows only a peer’s hint sends this command as a broadcast or multicast carrying a FILTER_NODE_HINT filter; a matching node replies with an encrypted unicast (or blind unicast) response carrying its full node identity. When the requester believes the peer may not yet know the requester’s own address, it sends its full source address in the request so the peer can reply directly.

Because a broadcast Identity Request can solicit many replies:

  • A node MAY decline to respond to a request from an unknown source.
  • A repeater MAY decline to forward a broadcast Identity Request, particularly when its filters are broad enough to solicit a large number of replies.

In order to manage the potential flood of responses, the following rules MUST be applied for broadcast (always) and (by default) multicast:

  • The Route option must either be absent or empty, otherwise the request should be dropped.
  • Responses must be delayed by a random amount of time drawn from a window of 0.5 to 30 seconds, so that the selected nodes do not all answer at once. A delayed response that then fails channel-activity assessment follows the responder’s normal bounded CCA backoff-and-retry before being dropped. A request every FILTER_NODE_HINT of which is 2 or 3 bytes is exempt and MUST be answered without the delay: such a request names a node rather than a share of the mesh, so there is no crowd of replies to spread and the hold would only make the answer late. One byte names a 256th of what the request reaches, so it is not exempt; filters of the same type combine as OR, and the shortest one decides.
  • One solicitation is answered at most once. A request reaches a node once per path it travels, and a plain broadcast carries no frame counter for a lower layer to recognize the repeat by; the NONCE is what names the solicitation. A responder suppresses a request matching one it has already answered—same sender, same NONCE—for at least as long as it may hold the reply. A request carrying no NONCE cannot be distinguished from a repeat of itself; a requester that wants a further answer inside that window asks with a fresh NONCE.

A FILTER_NODE_HINT filter names a single node, so a request carrying one solicits a single reply however far it travels. A partial hint names a small set rather than one node, and the shorter it is the larger that set: a requester sends the longest hint it holds. A request with no hint filter at all selects by role or capability, and every node it reaches may answer; such a request is therefore confined to the requester’s own neighborhood:

  • The FHOPS byte must either be absent or set to 0x00. A request that is flood routed—FHOPS present with either nibble nonzero—MUST NOT be answered.
  • The response MUST NOT carry a FHOPS field (the FCF flood hop count flag is clear). The invariant is that the reply travels exactly as far as the question did and no further: a request that crossed one hop is answered across that one hop, and a request steered along a Source Route option is answered back along the path it was steered down.

A request steered by a Route option reaches its destination neighborhood with that option emptied, since each repeater consumes its own hint. The nodes there are strangers to the requester: they hold no route to it, and their replies carry no flood budget for a repeater to spend. Such a request therefore carries a Trace option, and a node answering it sends the response as a source route built from the trace the request arrived with, when that trace is complete. Repeaters prepend when forwarding, so the accumulated trace already reads as the path back and is used as-is, without reversal.

The response carries a Trace option of its own. The trace on the request is what taught the responder a path home, and it taught the requester nothing: a broadcast solicitation leaves no route behind at the requester, and the response reaches it with its own Route option consumed and its FHOPS_ACC counting only the tail of the path. Without a trace on the response the requester ends the exchange holding no route to the node it just identified. A broadcast solicitation names no responder, so even one that arrived without flooding gives the requester no path to whoever answers it; the response traces regardless of the general rule.

A repeater consumes its own hint while forwarding, not while receiving, so a request whose Route option still names a repeater is one that repeater drops. Asking a named router to identify itself therefore means steering the request to the hop before it and narrowing it with that router’s two-byte hint: the request arrives with an empty Route option, and the router answers it as any other node in that neighborhood would.

These requirements MAY be relaxed for specific private channels, but MUST remain in place for all public channels. Note that these requirements are specifically designed to allow discovering identities from a specific repeater location on the mesh network.

An Identity Request is answered with a targeted unicast identity response, never by flooding an advertisement to the whole network.

Identity Request Options

A unicast Identity Request requires no payload. A multicast or broadcast request MUST carry at least one filter option, so that only the intended nodes respond.

Options use the CoAP-style delta-length encoding defined in Packet Options. As in CoAP, an option’s key encodes its criticality: odd-numbered keys (least-significant bit set) are critical, even-numbered keys are elective. A node that encounters a critical option it does not understand MUST treat itself as excluded and MUST NOT respond.

Filter options select which nodes respond. They combine as a logical AND across different filter types and a logical OR among repeated filters of the same type: a node responds only if it satisfies every filter type present, and it satisfies a given filter type if it matches any one of that type’s values. Non-filter options (such as NONCE) do not participate in this matching.

KeyCriticalNameValueDescription
1YesNONCE4 bytesCorrelation identifier the responder MUST echo in its response’s Nonce option. Not a filter.
3YesFILTER_NODE_HINT1–3 bytesRespond only if this is a leading part of the responder’s own node hint. A 3-byte value is the whole hint; a 2-byte value is the router hint, which is all a Source Route or Trace reveals about the hops it names. A zero-length value matches nothing.
5YesFILTER_NODE_ROLE1 byteRespond only if the responder’s primary role equals this value.
7YesFILTER_NODE_CAPS1 byteRespond only if the responder’s capability bitmap has every bit set that is set in this value.

Signal Report Request (2)

Requests that the destination respond with signal quality information about the link.

No command-specific payload.

Signal Report Response (3)

Reports signal quality measurements in response to a Signal Report Request.

FieldSizeDescription
RSSI1 byteReceived signal strength as an unsigned value representing negative dBm (e.g. 130 = -130 dBm)
SNR1 byteSignal-to-noise ratio as a signed value in dB

Echo Request (4)

Requests that the destination respond with an Echo Response.

FieldSizeDescription
Echo data0+ bytesArbitrary payload, copied verbatim into the Echo Response

Echo requests may be used for:

  • round-trip latency measurement
  • reachability testing
  • frame-counter synchronization (by observing the frame counter in the response’s SECINFO)

Echo Response (5)

Carries a response to a prior Echo Request, including any echo data from the request.

FieldSizeDescription
Echo data0+ bytesCopied verbatim from the Echo Request

The response SHOULD carry a Trace option when the request carried one. An echo measures a path, and a response traced differently from the request measures a different one; the per-hop signal in the two traces is what makes the measurement per-hop rather than end to end.

PFS Session Request (6)

Initiates a PFS session. The sender generates a fresh ephemeral node address and transmits it along with a requested session duration. See Perfect Forward Secrecy Sessions for the session establishment mechanism, key derivation, and wire-level privacy properties.

FieldSizeDescription
Ephemeral node address32 bytesSender’s newly generated ephemeral node address (Ed25519 public key) for this session
Session duration2 bytesRequested session lifetime in minutes; MUST NOT be 0

PFS Session Response (7)

Sent in response to a PFS Session Request. The responder generates its own ephemeral node address, returns it along with the accepted duration, and both sides derive session keys from the ephemeral addresses. See Perfect Forward Secrecy Sessions.

FieldSizeDescription
Ephemeral node address32 bytesResponder’s newly generated ephemeral node address (Ed25519 public key) for this session
Session duration2 bytesAccepted session lifetime in minutes; MUST NOT be 0

The responder MAY accept a shorter lifetime than was requested, and both parties hold the session to the accepted one. A node that receives a duration of 0 treats it as a finite lifetime of its own choosing; a responder answers with that lifetime.

End PFS Session (8)

Terminates an active PFS session. May be sent by either party. Upon receipt, both sides securely erase the private keys for their ephemeral addresses and revert to using their long-term keys. See Session Lifetime for all conditions under which a session ends.

No command-specific payload. The sender and recipient are identified by the packet’s addressing fields.

No-Op (9)

This command does nothing, however it will produce a UACK when sent via a packet type that requests an ACK.

Peer Repeaters Request (10)

Asks the destination—typically a repeater—for its list of known peer repeaters.

The list is the responder’s repeater neighborhood: the repeaters it has heard from their own transmitters. A repeater enters it in one of two ways. Forwarding a frame the responder receives is one—a repeater prepends its record to the frame’s Trace, so the first hint names the repeater whose transmission was just heard. Advertising is the other—a node identity claiming the repeater role or capability, heard directly rather than forwarded. Frames heard from nodes that are not repeaters, and identities that arrived through a repeater, put nothing in the list. Nor does anything naming the responder itself: a trace that begins with its own router hint, or its own identity. A neighbor need not be on the air: a repeater reached over a point-to-point link, such as an attached host, is listed the same way and simply carries no signal measurements.

The command-specific payload is a CoAP-style option list, using the delta-length encoding defined in Packet Options:

NumberNameValueDescription
0Nonce2 bytesCorrelation identifier the responder MUST echo in its response’s Nonce option.
1CursorvariableResume token, copied verbatim from the Cursor option of a previous response. Absent on the first request of an enumeration.

No payload follows the options, so the sender omits the 0xFF end-of-options marker.

A list too large for one response is retrieved page by page: the first request carries no Cursor, and each response that carries one names the place a follow-up request should resume from. The cursor is opaque to the requester; only the responder gives it meaning.

Peer Repeaters Response (11)

Returns one page of the responder’s peer-repeater list in response to a Peer Repeaters Request:

+---------+------+---------+------+---------+------+     +---------+--------+
| OPTIONS | 0xFF | ENTRY 0 | 0xFF | ENTRY 1 | 0xFF | ... | ENTRY N | (0xFF) |
+---------+------+---------+------+---------+------+     +---------+--------+

The response options:

NumberNameValueDescription
0Nonce2 bytesCopied verbatim from the request. Present only when the request carried a Nonce.
1CursorvariableOpaque resume token for the next page. Present when further entries remain; the final page carries no Cursor.
2Total1 byteUnsigned count of entries in the full list, not the page. Required in the response to a cursorless request; optional on later pages.

The 0xFF after the response options is the ordinary end-of-options marker; the entry list that follows is the command’s payload. Each entry is itself a CoAP-style option list terminated by its own 0xFF byte. The final entry MAY omit its terminator, consistent with omitting the marker when nothing follows.

The options per entry:

NumberNameValueDescription
0Node Hint2 or 3 bytesThe peer’s node hint—3 bytes when the responder holds it in full, or the 2-byte router hint when that is all it has observed. The only required option.
1Node NameUTF-8, max 24 bytesThe peer’s display name, as learned from its identity.
2RSSI/SNR2 bytesSignal measurements from the most recent reception: RSSI as an unsigned value representing negative dBm (e.g. 130 = −130 dBm), then SNR as a signed value in quarter-dB steps.
3Last Heard1–2 bytesMinutes since the responder last heard the peer, as a minimal big-endian unsigned integer. Saturates at 65535 (about 45 days).
4Location1–7 bytesThe peer’s position, in the variable-precision location format.
5Supported Flood Regionsn × 2 bytesConcatenated 2-byte region codes the peer flood-forwards for.

An option whose value the responder does not know is omitted. Entries carry region codes rather than the strings the identity option carries—the entry format is tighter on space, and the string form of a code, when one is wanted, is available from the peer’s own identity.

Verify Frame (12)

Asks the destination to vouch for a packet it sent to the requester whose frame counter the requester could not accept. See Counter Resynchronization for when it is sent and what each side does with the answer.

FieldSizeDescription
Nonce4 bytesChosen by the requester; the responder echoes it in Frame Verified

The packet carrying the command MUST include an Ack MIC option naming the packet to verify, and does not request an acknowledgement. A responder ignores a Verify Frame without the option.

Frame Verified (13)

Answers a Verify Frame naming a packet the responder sent and is still waiting to have acknowledged.

FieldSizeDescription
Nonce4 bytesCopied from the Verify Frame

The packet carrying the command MUST request an acknowledgement. Its acknowledgement acknowledges the verified packet as well.

Node Management

![NOTE] This section is an early work in progress and this protocol may change significantly.

A node that supports node management can be configured and observed over the mesh itself, using the same command grammar, property model, and numeric registries that ULCP defines for the local link. Node Management Request (payload type 8) and Node Management Response (payload type 9) payloads carry ordinary ULCP frames between an administrator—a node listed in the device’s administrator list—and the device, in unicast packets exchanged with the device identity. Support is optional and advertised through CAP_ADMIN.

Administering a device over the mesh reaches the device domain and nothing else. An administrator is not a tethered host: a remote exchange is not an attach, touches no session state, and neither sees nor disturbs the host domain or the assistance the device owes to whatever host it serves. Two Kinds of Attach draws the same boundary on the local link; over the mesh, only the administrative kind exists.

Transport Properties

The binding relies on exactly what the MAC layer guarantees for secure unicast: the source of every accepted packet is authenticated, payloads are confidential, and replay protection accepts a given frame at most once. It assumes nothing more—not delivery, and not ordering. The payload format adds what the ULCP grammar needs on such a transport:

  • a token correlates responses with requests across long and variable round trips, in place of the TID of the local bindings;
  • retained responses make retransmission safe: a repeated request that changes something is answered again, not executed again;
  • cursors carry a response larger than one frame across as many exchanges as needed, every fragment cut from that one response.

Several operations per exchange need nothing from the envelope: the multi-property commands already carry several reads, or several ordered writes, in a single frame.

Because wire-level duplicates are impossible, the binding has no deduplication window of its own; the only duplicates that can exist are an administrator’s own retransmissions, which the token identifies.

Payload Format

Request and Response payloads share one format, consisting of, following the payload type byte:

+-------+---------+------+----------+
| TOKEN | OPTIONS | 0xFF |  FRAME   |
+-------+---------+------+----------+
  2 B    variable   1 B    variable

Direction lives entirely in the payload type. A device drops a Response payload—it never solicits anything—and an administrator that receives a Response matching no outstanding exchange of its own discards it, both with accounting.

Node Management payloads travel only in unicast and blind unicast packets: Requests are addressed to the device identity, and Responses return to the requesting node on the carriage the Request arrived on (see Response Carriage), using whatever routing state the exchange has supplied (see Route Learning). A Response carries the Ack MIC option naming the Request it answers, so it confirms the Request’s delivery even when the MAC acknowledgment is lost. A device drops a Node Management payload arriving by multicast or broadcast, with accounting.

Token

Two opaque octets chosen by the administrator and echoed verbatim in the response. The token correlates a response with its request and identifies retransmissions (see Retries and At-Most-Once Processing). An administrator MUST choose a token different from its previous exchange’s when beginning a new exchange, and MUST reuse the token when retransmitting a request unchanged.

Options

Options use the CoAP-style delta-length encoding defined in Packet Options. As in MAC command options, odd-numbered options are critical and even-numbered options are elective. A device that receives a request carrying an unrecognized critical option answers with PROP_LAST_STATUS of STATUS_UNIMPLEMENTED and does not process the request; unrecognized elective options are ignored. An administrator that receives a response carrying an unrecognized critical option treats the exchange as failed.

NumberCriticalNameValue
1YesCURSOR1–8 octets, see Reading Large Values
2NoREMAININGPUI, see Reading Large Values

The 0xFF end-of-options marker is always present, since the frame follows.

Frame

Exactly one ULCP frame, extending to the end of the payload—the payload bounds it, so it carries no length prefix. The embedded frame uses the exact frame format of the local bindings, so a device dispatches it through the same machinery that serves its local link. Senders MUST set its TID bits to zero, and receivers ignore them: correlation is by token. A request whose frame is absent or cannot be parsed is answered with a CMD_PROP_IS of PROP_LAST_STATUS reporting STATUS_PARSE_ERROR.

The payload, envelope included, must fit a single UMSH frame; there is no fragmentation. The unassigned option numbers are this format’s growth space: a future need—carrying a request larger than one frame, say—is met by assigning a critical option, and existing devices already reject what they do not recognize.

Exchanges

Every interaction is an exchange: one request payload from an administrator, one response payload from the device. The device sends nothing over this binding except in response to a request—CMD_PROP_IS in its unsolicited role, and CMD_PROP_INSERTED and CMD_PROP_REMOVED as spontaneous notifications, do not occur here. State an administrator cares about is read, not pushed.

The Response echoes the token and carries one frame: exactly the frame the device would emit in reply on a local binding—a CMD_PROP_IS, CMD_PROP_INSERTED, CMD_PROP_REMOVED, CMD_PROP_ARE, or CMD_I2C_RESULT on success, or a CMD_PROP_IS of PROP_LAST_STATUS reporting the error. A request frame carrying a Device→Host command is answered STATUS_INVALID_COMMAND. Long-running operations report STATUS_IN_PROGRESS as on any binding; the administrator observes completion by reading state in a later exchange.

Multi-Property Requests

Several operations travel in one exchange through the multi-property commands, whose semantics this binding leaves untouched: CMD_PROP_MULTI_GET reads several properties, continuing past per-property failures, and CMD_PROP_MULTI_SET applies writes strictly in order, stopping at the first failure—which is how an administrator expresses writes whose effects depend on sequence. CAP_ADMIN requires CAP_CMD_MULTI, so an administrator may rely on both.

One rule is the binding’s own: the device does not execute a CMD_PROP_MULTI_SET entry whose reply entry would not fit the remaining space in the response payload, and stops there exactly as an error would stop it. An administrator that receives fewer reply entries than it sent examines the last entry it did receive: an error means the sequence stopped on that failure; a success means it stopped for space, and the administrator reissues the remainder as a new exchange.

Resets

Commands that initiate a reset—CMD_RST, CMD_RESTORE in its reset form, CMD_REBOOT, and CMD_FACTORY_RESET— are answered by no response payload. Delivery of such a command is confirmed by requesting a MAC acknowledgment, and its completion by a later exchange reading PROP_LAST_STATUS for the reset code.

A device that restarts is unreachable for as long as it takes to come back, and a repeater that restarts takes its stretch of the network with it. CMD_REBOOT is nonetheless within an administrator’s reach, because the node it is most worth sending to is the one nobody can walk to.

DFU Entry

CMD_DFU uses an ordinary request/response exchange with TID zero and the request token in the Node Management Response. A MAC acknowledgment confirms delivery only; it does not confirm DFU acceptance. The device finishes fallible preparation and persists replay-counter state before emitting STATUS_OK, then enters the bootloader after that specific response transmits. No additional wire acknowledgment is requested for the response.

A pending handoff belongs to its retained reply. Retries cannot initiate another handoff. If transmission fails, is canceled, or exceeds the completion deadline, the device remains running and replaces the retained success with a failure, so a later retry cannot falsely confirm entry. Normal administrator authorization applies; DFU preserves saved settings and credentials. The administrator stops operating on the target after confirmed entry, while its companion-radio connection remains intact.

Retries and At-Most-Once Processing

The MAC layer’s replay protection means a device never receives the same request frame twice; what it can receive twice is the same request sent twice—an administrator retransmitting because no response arrived, though the request may in fact have been executed. Executing a request that changes something twice would be wrong, so for every request other than a read the device retains, per administrator, the token and the complete response of the most recent exchange. A request whose token matches the retained token is answered by retransmitting the retained response, without executing anything.

A read (CMD_PROP_GET or CMD_PROP_MULTI_GET) changes nothing, so its response is not retained: a retransmitted read is executed again and answered with current values. The exception is a read whose response does not fit one payload. Its whole response is retained, and each fragment is cut from it (see Reading Large Values).

Each new exchange ends the one before it. When an administrator begins an exchange under a different token, the device discards that administrator’s retained entry, whether or not the new exchange is retained in turn; only a continuation carries the read it continues forward. A device MAY bound what it retains, in entries or in octets, evicting the least recently active administrator’s entry first, but retains at least the entry for the most recently active administrator.

An administrator requests a MAC acknowledgment for every request, and recovering a lost request is the MAC’s work: its retransmissions and Route Retry. An administrator MUST NOT retransmit a request whose delivery the MAC has not yet resolved, since a retransmission is a new packet that repeaters forward again. A request the MAC fails to deliver ends the exchange. A request that is acknowledged but not answered is retransmitted, identical and under the identical token, once its response is overdue for the path’s round-trip behavior. This is safe whether or not the device executed the request: a retained response is repeated, and a read is answered afresh. An administrator MUST NOT have more than one exchange outstanding with a given device.

Retained entries do not survive a reset. A reset command retransmitted after it has already acted is therefore executed again—with the same result.

Reading Large Values

A read whose response does not fit one payload is completed across several exchanges. This applies to both read requests: a CMD_PROP_GET whose value does not fit, and a CMD_PROP_MULTI_GET whose entry list does not fit. The response frame is well-formed, but its trailing content (the value of the CMD_PROP_IS, or the entry list of the CMD_PROP_ARE) is a leading fragment, accompanied by a CURSOR option: an opaque continuation handle, one to eight octets, chosen entirely by the device. The administrator continues with a new exchange, under a fresh token, whose request carries the returned cursor verbatim alongside a repeat of the request being continued. Each response carries the cursor to present in the next request; a response without one ends the read, its fragment being the last. Fragment sizes are the device’s choice, made to fill each frame; there is no fixed block size and no position numbering.

The request is executed once, by the exchange that begins the read. The device retains that complete response and answers each continuation with the next fragment cut from it, executing nothing. The reassembled value or entry list is therefore exactly what one execution produced, however the device’s state changes between exchanges.

A request carrying a CURSOR option MUST be the read being continued—the same CMD_PROP_GET or CMD_PROP_MULTI_GET that began it. A cursor on any other request is answered STATUS_INVALID_ARGUMENT.

The contract:

  • A cursor is meaningful only to the device that issued it, and only for the request it was issued for. The administrator returns it byte-for-byte and MUST NOT construct or modify one.
  • Fragment boundaries are the device’s choice and carry no meaning. The administrator reassembles the read by concatenating the fragments in order and parses the whole: a property value under the property’s own rules, an entry list under CMD_PROP_ARE’s.
  • Presenting the same cursor again yields a fragment cut from the same response at the same position; a retransmitted continuation is in any case answered from the retained response (see Retries and At-Most-Once Processing).
  • Cursors are untrusted input. The device validates every cursor it receives and answers one it cannot honor (it does not parse, it was issued for a different request, or the response it continues is no longer retained) with STATUS_CURSOR_INVALID (see Status Codes); the administrator restarts from a cursor-less request.
  • A cursor is honored only while the response it continues is retained. Any other exchange from the same administrator ends the read, a fresh start of the same read included, and eviction or a reset discards it.
  • A response MAY carry a REMAINING option: the approximate number of octets not yet returned, as a packed unsigned integer. It is advisory, for progress reporting.
  • Between exchanges, the position lives entirely in the cursor the administrator holds; the device keeps nothing for the read beyond the retained response.

Authorization

A device executes a Node Management request only when the packet arrived by unicast or blind unicast, its source is authenticated by the MAC layer, and the source’s public key is listed in PROP_DEV_ADMINS. Everything else it drops, with accounting and without a response: an unlisted sender learns nothing about whether the device is manageable.

Administrators read everything: CMD_PROP_GET is never refused for lack of standing, and a property the device does not serve fails exactly as it would on the local link. PROP_CAPS in particular is readable, so capability discovery works exactly as on the local link.

Writes are narrower. A device answers STATUS_NOT_PERMITTED—the operation exists, and the binding is what refused it—to:

  • CMD_PROP_SET, CMD_PROP_INSERT, and CMD_PROP_REMOVE naming session state or the host domain (see State Classes): that state belongs to the tethered host, and an administrator is not one. The exception is PROP_MAC_BACKHAUL: which side of the radio multiplexer the tethered host sits on is worth flipping from across the mesh, so an administrator may write it, though the host’s next attach still resets it;
  • CMD_STR_SEND and CMD_QUEUE_DRAIN: the raw PHY stream and the receive queue are likewise the tethered host’s;
  • writes to PROP_DEV_PRIVATE_KEY: a device identity cannot be installed over the mesh.

For everything else this binding meets the transport requirement of Provisioning Security: every executed request already arrives authenticated and encrypted from a listed administrator, so device-domain key material—channel keys, peer entries —may be provisioned remotely. The read-back rules are unchanged: key-bearing properties report their digest forms, never secrets.

PROP 4865: PROP_DEV_ADMINS

  • Type: Multiple-Value
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_ADMIN
  • Item Type: 32 octets
  • Post-Reset Value: Empty

Each item is the Ed25519 public key of a node authorized as an administrator of this device. Items are public keys: they carry no secret material and are reported verbatim.

An empty list disables node management entirely and is the post-reset default. The property is device-domain state: it participates in the saved snapshot like any other device-domain property, which is how a commissioned repeater stays manageable across a power cycle. It is writable over the local bindings and over this one—a listed administrator may add or remove administrators, itself included.

Capabilities

CodeNameRequiresGrants
43CAP_ADMINCAP_DEV_IDENTITY, CAP_CMD_MULTINode management: processing of Node Management Request payloads addressed to the device identity, and PROP_DEV_ADMINS

A device that does not advertise CAP_ADMIN drops Node Management payloads, with accounting.

Text Messages

![NOTE] This section is an early work in progress and this protocol may change significantly.

The text message protocol carries human-readable text between nodes (unicast) or from a node to a channel (multicast).

The payload consists of a CoAP-style option list terminated by a 0xFF byte, followed by the message body. All options are non-critical—unrecognized options are ignored and the remainder of the message is displayed normally.

Message Options

Every recognized option is a singleton unless a specification explicitly declares otherwise. How a receiver treats a duplicated recognized option depends on the option’s role:

  • An option that carries identity, sequencing, or reference semantics—Message Type, Message Sequence, Regarding, Editing, and extension options in the same role such as Sender Sequence—makes the message invalid when repeated, even if the repeated values are identical. The message MUST be dropped.
  • A presentation option—Sender Handle, Background Color, Text Color—keeps the first occurrence when repeated; later occurrences are ignored and MAY be reported diagnostically.
  • Duplicates of a zero-length flag option—Sequence Reset, Channel Group Resend—are idempotent.

Repeated unrecognized options remain ignorable.

A recognized option whose value the receiver cannot decode is ignored, with two exceptions. A message whose Message Sequence cannot be decoded is dropped. A message whose Message Type is unrecognized or undecodable, or whose Regarding or Editing is undecodable or of the wrong width for the conversation, is not displayed, but its sequence position is accounted for as with Message Unavailable.

NumberNameValue
0Message Type0 or 1 bytes
1Sender HandleUTF-8 string
2Message Sequence1 or 3 bytes (see below)
3Sequence Reset0 bytes (flag)
4Regarding1 or 4 bytes (see below)
5Editing1 byte
6Background Color3 bytes, RGB
7Text Color3 bytes, RGB
8Channel Group Resend0 bytes (flag)
9PaddingAnything
10Timestamp Sent4-byte unix timestamp

Message Type

ValueNameRendering
0Basic textDisplayed in a text bubble
1Status textDisplayed inline as “[HANDLE] [MESSAGE]” (similar to IRC /me)
2Message Resend RequestNot displayed; see the body rules below
3Message UnavailableNot normally displayed; see below

If absent or empty, the message type defaults to 0 (basic text).

The presence of the Regarding option changes the semantics a bit: A reply is a type 0 message with a Regarding option specifying which message is being replied to. A reaction is a type 1 message with a Regarding option specifying which message is being reacted to; the body is a single Unicode emoji or a short text token such as +1, -1, !, or ?. Implementations may differentiate reactions from plain status text by the presence of the Regarding option.

Reaction bodies SHOULD be short text tokens rather than emoji: <3, +1, -1, ha, !, and ? cover the common reactions in one or two bytes. Receivers SHOULD accept the equivalent spellings—♥ for <3, haha or lol for ha, !! for !, letter case being insignificant—as well as the emoji themselves, and SHOULD render a reaction as a single glyph, taking the first extended grapheme cluster (UAX #29) when a body carries more, so that a sequence such as 👍🏽 or 🇫🇷 stays whole.

There are six canonical reactions:

NameEmojiAcceptable ASCII Representations
Approve👍+1
Disapprove👎-1
Love❤️<3
Haha🤣ha, lol, haha
Surprise‼️!!, !
Question❓?

A sender has at most one reaction in force per message. Sending another supersedes the previous one, and a reaction with a zero-length body withdraws it; a receiver displays the most recently received reaction from each sender (unless it was withdrawn).

Reactions reference the original message’s ID even when the message has since been edited, as Editing requires of every reference. A receiver MAY additionally tolerate a reaction that names an edit’s own ID by resolving it to the message that edit replaced.

A message resend request is itself not a message but a request to re-send a message that was inferred to exist but was not received. Such a request SHALL contain exactly one Message Type option with value 2 and exactly one Message Sequence option identifying the missing frame. A request with duplicate Message Type or Message Sequence options is invalid and MUST be ignored. It MAY also contain the Channel Group Resend flag described below and SHOULD NOT contain any other options. After validating the required options and the Channel Group Resend flag, receivers MUST ignore every other option and the entire body. Senders SHOULD use an empty body. The request MUST be received via unicast (for one-on-one messages) or blind-unicast (for channel messages), and SHALL be dropped if received via broadcast or multicast.

A resend request identifies exactly one missing frame, and a successful response retransmits exactly one frame. The 1-byte Message Sequence form requests a message by ID without specifying a fragment; the sender responds with the unfragmented message or, if it was fragmented, with fragment zero. The fragment count carried in fragment zero lets the requester ask for any remaining fragments individually. The 3-byte form requests one specific fragment.

Responses to a message resend request are optional and MAY be ignored.

Not all messages are available to be resent; for example, a requested message may have been deleted or evicted. In that case, the original sender SHOULD respond with Message Unavailable. A sender producing Message Unavailable SHOULD include only a Message Type option with value 3 and one Message Sequence option exactly matching the requested message or fragment. Its body SHOULD be empty.

Message Unavailable follows the delivery path appropriate to the requested conversation, just like a successful resend of the original message: direct-conversation responses use unicast, blind-unicast one-to-one channel responses use blind-unicast, and channel-group responses use multicast to the channel.

Once a receiver has unambiguously recognized Message Unavailable and its single Message Sequence option, it MUST ignore all other options and the entire body. The indicated sequence position is treated as accounted for, so it no longer remains a gap in that sender’s sequence history. The receiver is not required to render a whole-message unavailable response in the UI.

When the three-byte Message Sequence form identifies a fragment, only that requested fragment is reported unavailable, even if the sender no longer has any part of the message. Other fragments remain independently repairable. The receiver SHOULD render the unavailable portion of the partial message as unavailable rather than continuing to show it as pending.

Message Unavailable is also the standard response to a request for an unknown Message Sequence ID, including an ID that has not been sent.

Channel Group Resend

A zero-length flag used only on a Message Resend Request received by blind-unicast. Both a request for a message missed from a multicast channel conversation and a request for a message missed from a blind-unicast one-to-one conversation arrive by blind-unicast. The presence of this flag selects the sender’s archive for the multicast channel conversation. Its absence selects the sender’s archive for the blind-unicast one-to-one conversation with the requester.

A receiver MUST drop a request containing this flag if the request was not received by blind-unicast or if the option has a non-zero length. The flag has no meaning on other message types and MUST be ignored there.

Sender Handle

A UTF-8 string containing the name or pseudonym of the sender. If not supplied, a handle may be inferred from previously received node metadata for the sender’s address.

Message Sequence

Associates a packet with a monotonically increasing message identifier maintained independently for each sender within each conversation, and optionally carries fragmentation state. The option is encouraged on all messages but is not required. Messages without this option cannot be directly referenced in replies or reactions.

The option value is either 1 byte or 3 bytes:

1-byte form (message ID only):

ByteFieldDescription
0Message IDPer-sender message identifier (wraps at 255)

3-byte form (fragment):

ByteFieldDescription
0Message IDShared by all fragments of the same message
1Fragment IndexZero-based position of this fragment
2Fragment CountTotal number of fragments (must be 2 or greater)

Rules:

  • Message IDs are monotonically increasing per sender within each conversation, wrapping at 255. A one-to-one blind-unicast conversation and the group conversation on the same channel are distinct conversations with independent sequence streams, even though they share channel key material.
  • When an ID wraps and is reused, the older message with that ID in the same conversation-and-sender stream is retired: it is no longer a valid target for Regarding, Editing, or a resend request in that stream. Retirement does not affect any other stream or already-displayed history.
  • Messages smaller than MTU-32 bytes SHOULD NOT be fragmented.
  • A fragment body carries at most 160 bytes, and a message has at most 10 fragments; senders MUST NOT exceed either limit. Larger fragment bodies and larger fragment counts are nevertheless syntactically valid; a receiver that will not reassemble such a message drops the assembly and MAY account for the ID as unavailable rather than leaving a gap.
  • Fragmentation splits the body at byte boundaries. An individual fragment body is not required to be valid UTF-8; the reassembled body is validated as UTF-8 only after every fragment is present.
  • Options from the first fragment (Fragment Index 0) apply to the entire reassembled message. Subsequent fragments MUST NOT include options that would override those of the first fragment, and any such options MUST be ignored by the receiver.
  • During reassembly, missing fragments SHOULD be rendered as [FRAGMENT MISSING], or an appropriately-localized equivalent. When a fragment boundary splits a UTF-8 code point, rendering discards the incomplete code point on each side of the gap; the substituted marker is part of the rendered display, not the message body.
  • Out-of-order reassembly SHOULD be supported for fragments received within a reasonable amount of time (thirty seconds to two minutes).
  • Edits (see Editing) carry their own message IDs and MUST NOT be referenced by subsequent Editing or Regarding options. The original message ID is the stable reference.

Ordering, Gaps, and Automatic Repair

Message IDs compare using serial-number arithmetic modulo 256. Relative to the most recent ID accepted from a sender in a conversation, a forward delta of 1 through 127 is newer; a delta of 128 through 255 is old or ambiguous.

A forward delta greater than 1 within one conversation-and-sender stream means messages are missing from that stream. A gap in one sender’s stream says nothing about any other sender in the conversation. After a short reordering grace period, a receiver MAY request each missing message with a resend request using the 1-byte Message Sequence form, subject to the limits below:

  • Automatic repair is bounded: a receiver SHOULD NOT automatically request more than 8 missing messages from a single observed gap, and SHOULD apply per-peer and overall rate limits to all generated resend requests.
  • A forward delta greater than the receiver’s automatic-repair bound, an ambiguous delta, the first message observed from a sender, and the first message after a Sequence Reset all establish a new baseline. Receivers MUST NOT generate automatic resend requests to backfill across a baseline.
  • Repair of a channel-group conversation requires addressing a blind unicast to the original sender, which requires that sender’s full public key. A member holding only the source hint cannot construct the request and simply renders the loss.
  • In a channel-group conversation every member observes the same loss at nearly the same moment. Receivers MUST delay each automatic group repair request by an independently randomized interval, and MUST cancel a pending request when the missing message—or a Message Unavailable naming it—arrives on the channel before the request is sent.
  • Room conversations are unicast; room repair requests are sent directly to the room without group jitter, and responses repair only the requester.

A repaired message occupies its position in sequence order within the sender’s stream, not the position at which it happened to arrive. A receiver MAY reserve that ordered position when the gap is first observed—rendering a pending-gap placeholder there while a repair is outstanding—and fill the same position in place when the message arrives, rather than appending it at the end of the transcript. A message that fills a reserved position after later messages are already displayed MAY be marked as having arrived late. A gap whose repair is ultimately exhausted, expired, or disclaimed by the sender MAY be presented as an unavailable position rather than silently removed.

When a message is delivered in fragments, a receiver SHOULD raise any user-facing notification once the final fragment is received, or once a bounded latency (on the order of thirty seconds) has elapsed since the first fragment—whichever comes first—so a stalled reassembly still notifies without waiting for the full reassembly lifetime.

Sequence Reset

A 0-byte flag option that signals the sender has reset its message ID counter for the current conversation—for example, after losing that conversation’s persistent sequence state. Receivers SHOULD discard cached message context for this sender in this conversation, including pending fragment reassembly state. State for the same sender in other conversations is unaffected.

The Sequence Reset option SHOULD accompany a Message Sequence option bearing the sender’s new starting ID. In the absence of a Message Sequence option, receivers SHOULD treat the next message from that sender as starting a fresh sequence.

Reset announcement is lazy and scoped to the affected conversation: the sender includes the flag on the next message it actually sends in that conversation. Senders do not transmit standalone reset messages preemptively to conversations they are not otherwise sending to.

Regarding

References a previously sent message for the purposes of replies and reactions.

The option length depends on the conversation, not on how the MAC packet is addressed:

  • One-to-one conversation (unicast, or a blind-unicast conversation with a single logical destination): 1 byte—the Message ID of the referenced message.
  • Channel-group conversation (delivered by multicast to the channel): 4 bytes—the 1-byte Message ID followed by the first 3 bytes of the source public key of the original sender.

The source prefix is necessary in multicast channels to disambiguate messages from different senders that may share the same Message ID. This means a message cannot be referenced if it is more than 255 messages old in that sender’s sequence, or if the user has since reset their sequence ID.

In chat rooms, the room assigns canonical Message IDs across all senders (see Chat Rooms), so the 1-byte form is used and no source prefix is needed.

Editing

Indicates that this message replaces a previously sent message in the same conversation and sender scope. The option value is 1 byte: the Message ID of the message being edited. No source prefix is needed because the source scope comes from the enclosing packet. For unicast and blind-unicast, pairwise authentication binds that source to a peer key. For multicast, the MIC proves only possession of the channel key; the source is a claimed identity, and one channel member can impersonate another. A multicast whose frame signature verifies is the exception: the signature binds it to the signer’s key.

An edit with a zero-length body signals deletion of the original message.

For as long as a client retains sequence history for an edited or deleted message, it SHOULD retain the stable original-message identity or a tombstone so that references can still be resolved. A client MAY retain prior revision content, including deleted content, for edit-history display; this is a local storage and privacy policy rather than a protocol requirement.

Edit messages carry their own Message IDs. References in subsequent Editing or Regarding options MUST use the original message’s ID, not the edit’s ID.

Once a message has been edited or deleted, its superseded content MUST NOT be retransmitted: a resend request naming the original Message ID is answered with the current content re-issued under that ID, or with Message Unavailable—never with the pre-edit bytes. This obligation is durable; it survives restarts of the sending node. Retaining superseded content locally for the sender’s own review remains permitted, per the retention note above.

A receiver holding an unrepaired gap at an edited message’s original ID MAY treat an arriving edit that references it as satisfying that gap—the edit already carries the position’s current content—and cancel the pending repair rather than requesting content the sender is no longer willing to send.

How edits are presented to users is implementation-defined. Implementations typically display only the most recent edit, with some indication that edits exist, and an optional mechanism to view edit history.

Background Color

Three bytes (red, green, blue) specifying a suggested background color for the text bubble. Receivers may ignore this option. If supported, implementations should ensure adequate contrast with the text color.

Text Color

Three bytes (red, green, blue) specifying a suggested text color. Receivers may ignore this option. If supported, implementations should ensure adequate contrast with the background color.

Padding

The padding option is used to add padding to a message to obfuscate the true length of the message. All instances of this option and their values SHALL be ignored by the receiver.

Message Body

The message body is a UTF-8 string.

Channel Invitations

A basic text message whose entire body, ignoring leading and trailing whitespace, is a channel URI invites its recipients to that channel. A receiver that recognizes the URI MAY present the message as an invitation to join; one that does not displays the body as text.

Test Messages

A basic or status text message on the EMERGENCY channel is a test message when its body, ignoring leading whitespace, begins with TEST: in any ASCII letter case. For a fragmented message, the body of fragment zero decides.

Unless the user has opted in to test messages, a receiver SHOULD NOT raise any alert for a test message: no sound, vibration, notification, screen wake, or unread indication. The message is still stored and displayed.

Chat Rooms

![NOTE] This section is an early work in progress and this protocol may change significantly.

A chat room is a special node that provides limited store-and-forward capability for text messages and potentially other types of data. Chat rooms may be polled or can push updates to joined members.

Action Types

The first byte of the payload identifies the action type.

ValueActionDirection
0Get Room InfoUser → Room
1Room InfoRoom → User
2LoginUser → Room
3LogoutUser → Room
5Fetch MessagesUser → Room
6Fetch UsersUser → Room
7Admin CommandsUser → Room
8Room UpdateRoom → User

Regular message exchange—including system events—does not use a dedicated action type. Users send messages to the room as plain text message payloads by unicast, and the room distributes them to each member by separate unicast transmissions. Chat rooms do not use multicast. Action types are reserved for room management operations. Room Update (action 8) is used only for batch history delivery and is also delivered by unicast.

Get Room Info / Room Info

A user may send a Get Room Info action to a room node without being logged in. The room responds with a Room Info action containing CoAP-option-encoded metadata:

NumberNameNotes
0Room NameUTF-8 string
1Owner Information
2AdministratorUser ID; may appear more than once. May only be included for logged-in users
3Active User Count
4Max User Count
5Message Queue Depth
6Most Recent Message Timestamp
7Oldest Retrievable Message Timestamp

If the options are terminated with a 0xFF byte, the remainder of the response is a UTF-8 description of the room.

Login

The login payload is CoAP-option-encoded:

NumberNameNotes
0HandleIf omitted, the room uses the previous handle or assigns one
1Last Message TimestampIf present, the room sends up to 10 missed messages since this time
2Session TimeoutRequested inactivity timeout in minutes (1 byte)
3PasswordRequired only if the room is password-protected and the user’s public key is not already known

All options are optional. Behavior details:

  • If a last-message timestamp is provided and more than 10 messages have been received since then, only the 10 most recent are sent automatically. Older messages MAY be retrieved with Fetch Messages, if stored.
  • If the room is password-protected but already recognizes the user’s public key, the password is ignored. A room may forget a public key after prolonged inactivity, requiring a password on the next login.
  • First-time logins must include the full 32-byte public key (S flag set).

Logout

Logging out unsubscribes the user from push updates and removes them from the active user list. Previously sent messages remain stored and retrievable by other users up to the history limit.

If the user is currently logged in, the room sends a final text message to all members using the User Left message type (see System Events).

Send Message

To send a message to a chat room, a user sends a standard text message unicast to the room node. The Sender Handle option is ignored—the room fills it in from the sender’s registered handle when distributing the message to members.

The Message Sequence option SHOULD be included, using the sender’s own sequence for its conversation with this room. The room uses this to detect duplicate submissions and to order rapid messages from the same user. This sender-assigned ID is separate from the canonical room-assigned ID that the room assigns when distributing the message.

Fetch Messages

Retrieves previous messages posted to the room, which may include system messages.

FieldSizeDescription
Timestamp4 bytesFetch messages up to and including this time
Max Count1 byteMaximum number of messages to return

Fetch Users

Retrieves the currently active user list, possibly including their public keys.

Admin Commands

TBD.

Message Distribution

When the room receives a message from a user, it assigns a monotonically increasing canonical Message Sequence ID from a single room-wide counter and distributes the message to each logged-in member as a separate unicast text message. System events (user join/leave, admin messages) are distributed the same way, using room-specific message types. This gives the room a single unified message ordering across all activity—a Regarding option referencing a room message ID is unambiguous without a source prefix (see Regarding).

All timestamps are managed by the room and are relative to its own clock—typically a UTC UNIX timestamp, though accuracy depends on whether the room’s clock is synchronized.

Sender Sequence

When the room echoes a message back to the original sender, it faces a correlation problem: the sender showed the message optimistically in their UI the moment they sent it, but the echoed copy arrives with a room-assigned ID the client has never seen. Without some way to link them, the client cannot reliably identify which pending outbound message the echo corresponds to—matching by content alone fails if the user sends identical messages in quick succession.

To solve this, the room includes a Sender Sequence option on the echo it sends back to the original sender. This option is not included in copies sent to other members.

NumberNameValue
12Timestamp Received4 bytes, UTC UNIX timestamp
13Sender Sequence1 byte—the sender’s original Message Sequence ID

The Sender Sequence value is the per-sender Message Sequence ID the user included in their outbound message. The client matches this value against its pending outbound messages to identify the echo. It then retains both wire identities on the same local message record: the sender-assigned ID and the canonical room-assigned Message Sequence ID carried by the echo. Room-wide Regarding references use the canonical room-assigned ID. When the original sender submits an edit or deletion of its own message to the room, its Editing option uses the original sender-assigned ID so that the room can correlate the update before redistributing it with the appropriate canonical reference.

System Events

The room delivers system notifications as text messages to all logged-in members, using message types reserved for room use:

ValueName
32User Joined
33User Left
34Admin Message

The Sender Handle option is automatically populated by the room for all distributed messages, including system events.

Room Update

Room Update is used exclusively for batch delivery: the history sent on login (via the Last Message Timestamp login option) and the response to Fetch Messages. It contains a list of length-prefixed text messages in chronological order, each carrying the room-injected options defined above (Timestamp Received, and Sender Sequence where applicable). This batching avoids the per-packet overhead of sending history as individual unicast messages on LoRa.

ValueActionDirection
8Room UpdateRoom → User

URI Formats

UMSH defines URI forms for nodes, channels, and CoAP resources.

Node URIs

Nodes are identified by their 32-byte public key encoded in Base58.

Example:

umsh:n:HJC9DJaaQEn88tAzbMM7BrYbsepNEB69RK1gZiKEYCPp

Node identity information may optionally be appended after a colon, encoded in a suitable representation of the identity structure and optional signature.

Example:

umsh:n:HJC9DJaaQEn88tAzbMM7BrYbsepNEB69RK1gZiKEYCPp:Rgx5U993cN52iHc9rPEFPpLTB66o2JLaDvSpCxmhPdReNd3QtrYcyrACdWV89L1xfZPJz4rZGeHX9BypGtDDYJXbDrWKJZixp9A8d3qcDNFq

This allows a node identity bundle to be embedded in a QR code.

Channel URIs

Internally, channels are identified by a 32-byte shared key.

Example direct-key URI:

umsh:ck:5BFn8YGKJ6pZR4qV3tW7mNhDrXsCxEaL9kUv2wAjT8bP

Additional metadata may be attached as URI parameters:

umsh:ck:5BFn8YGKJ6pZR4qV3tW7mNhDrXsCxEaL9kUv2wAjT8bP?n=MyPrivateChannel;mh=6;r=Eugene

Where, for example:

  • n = channel name
  • mh = recommended maximum flood hops
  • r = recommended region

A channel may also be identified by a string from which the channel key is derived:

umsh:cs:public

The name that follows umsh:cs: is canonicalized before key derivation: it is percent-decoded, required to be ASCII, and folded to lowercase (see Named Channels). Because of the fold, umsh:cs:public, umsh:cs:Public, and umsh:cs:PUBLIC all identify the same channel.

CoAP-over-UMSH URIs

CoAP resources on a node use the coap-umsh scheme, with the node public key as the authority component.

Example:

coap-umsh://HJC9DJaaQEn88tAzbMM7BrYbsepNEB69RK1gZiKEYCPp/data/1

Local Control Protocol (ULCP)

![NOTE] This section is an early work in progress and this protocol may change significantly.

The UMSH Local Control Protocol (ULCP) is the interface between a UMSH device—hardware that owns a physical transceiver and runs always-on firmware—and a host such as a phone, tablet, laptop, or small computer that configures or uses it over a local out-of-band link. The device is not merely a dumb modem, but it is also not normally the primary home of the user’s long-term UMSH identity.

This chapter describes the architecture: what the device and host each are, where the security boundary sits, and which responsibilities belong to each side. The protocol itself is specified in the chapters that follow:

One protocol serves every deployment of a device; the familiar deployment names describe configuration, not distinct firmware:

  • a companion radio is a device tethered to a host that owns the user’s long-term identity—the device owns the physical LoRa transceiver, and it may also host a local device-owned node for management and diagnostics
  • a repeater is a device commissioned over the same protocol and then left to run autonomously, forwarding traffic with no host attached

This differs from systems where the radio itself is the user’s primary mesh identity. In UMSH, the user-facing identity usually lives on the host device, not in the radio.

That separation has important consequences:

  • the device does not hold the user’s long-term private key
  • the host device remains the authority for user identity, contacts, and high-level application behavior
  • the device may still perform some limited actions while disconnected if the host has provisioned the necessary state in advance

Identities

A device deals with at most two node identities:

The Device Identity

The device hosts a node that belongs to the device itself. This node exists even when no phone is attached and can be used for:

  • in-band management
  • diagnostics
  • repeater or bridge behavior
  • announcing the presence or capabilities of the device
  • on a device with a messaging interface of its own, the messaging identity of whoever carries it

By default, such a node need not advertise itself with ordinary beacons. Its private key is either generated on the device itself (preferred) or installed once by the host; it is never readable back over the ULCP link. The host can read the corresponding public key at any time.

The device identity, its channel keys, its peer list, and the device’s own settings (RF configuration, and eventually repeater policy, positioning, and advertisement behavior) form the device domain: state that belongs to the device and survives a change of host.

The Tethered Host Identity

A phone or computer attaches to the device and uses it as its radio interface. The host’s UMSH identity remains on the host: the device learns only the identity’s public key, forwards traffic for it, and may cache narrowly scoped related state (channel keys, per-peer symmetric keys, queued inbound frames).

ULCP supports exactly one tethered host identity at a time. A host application that manages multiple user identities is expected to select one for the device to assist; it can still send and receive traffic for others through the raw frame stream while attached.

Everything provisioned for the host identity—its public key, channel keys, peer keys, filters, and queued inbound traffic—forms the host domain: state that is keyed by the host identity and wiped wholesale when a different host identity takes over the device. Pairing the device with a new phone therefore starts the host state over cleanly while leaving the device’s own identity and settings untouched.

Operating Modes

Tethered

In tethered mode, a host device uses the device almost as though it were a local hardware peripheral. This is the most direct mode and is expected to be the common case for phones. It is the mode the ULCP protocol chapters specify, and a device deployed this way is a companion radio.

Tethered mode supports:

  • radio configuration
  • raw UMSH frame transmit and receive
  • receive filtering so the host is not woken for irrelevant traffic
  • optional offline assistance when the host disconnects

Bridged

In bridged mode, the device behaves more like an infrastructure service. One or more nearby devices may submit traffic through it, or it may forward traffic on their behalf subject to local policy, without being the primary owner of the identities using it.

Bridged mode is useful for:

  • a fixed radio shared by multiple users in one location
  • a site gateway that extends range for nearby devices
  • deployments where the host device is intermittent but the radio remains on

Bridged mode should not be confused with tethered ULCP. Tethering is one host talking to its own companion radio over a local control link. Bridging is a separate local access problem in which nearby devices are treated more like peers or clients of the device itself. Bridging is not yet specified; see BLE As A Local Bearer for the design space.

Hybrid Use

A real device may use both modes at once. For example, a phone is tethered for its user’s personal traffic while the device’s own local node remains available for management. ULCP therefore must not assume exclusivity.

Two Kinds of Attach

A host on the ULCP link is doing one of two things, and the difference is not a mode of the device but a description of the host’s intent. The device does not track it; the host knows which it means and confines itself accordingly.

Tethered attach. The host is this device’s host. It writes PROP_HOST_KEY, provisions the host domain—channel keys, peer keys, receive filters, delegation policy—and thereafter the device filters, queues, and acknowledges on its behalf. A device serves at most one tethered host at a time, and being tethered is a transient local relationship: it does not appear in the device’s node identity, does not survive a power cycle, and is re-established on every attach.

Administrative attach. The host is configuring the device: its own identity, its radio, its behavior, its saved snapshot. It writes nothing in the host domain. This is what commissioning a repeater is, and it is the normal relationship for any device that is not somebody’s radio.

The rule that follows is short: configuring a device’s device domain MUST NOT claim its host domain. One phone administering ten repeaters must not write PROP_HOST_KEY on any of them—it would make each repeater start filtering and queueing for a host that has no intention of coming back, and would displace whatever host the repeater was actually serving.

Nothing in the protocol distinguishes the two: a host that has reached the link can do either (see ULCP over BLE). The distinction belongs in host implementations, which SHOULD make it explicit rather than incidental—an administrative handle that refuses host-domain writes cannot commit this error by accident.

Security Boundary

The fundamental rule is:

A device must not be provisioned with private keys owned by another device.

In particular, the host device keeps ownership of its own long-term and ephemeral private keys. This keeps the device from becoming an alternate trust anchor for the user’s identity and reduces the impact of device compromise, theft, or firmware bugs.

However, the device may still be provisioned with some additional keying material, depending on what offline behavior is desired.

Material That May Be Provisioned

A host may choose to provision the device with:

  • multicast channel keys
  • pairwise symmetric keys derived for specific peers
  • receive filters tied to specific identities, hints, or packet classes

Material That Must Not Be Provisioned

ULCP must not provide a mechanism for provisioning:

  • any private key owned by the host device

Material That Should Generally Be Avoided

Implementations should also avoid provisioning:

  • broad contact databases unrelated to radio operation

Why Provision Keys At All?

If the device does not have the host’s private key, it cannot perform fresh ECDH on the host’s behalf. That means it cannot derive new pairwise state for previously unknown peers by itself.

Nevertheless, there are useful cases where the host may deliberately preload symmetric key material for specific already-known peers and channels:

  • Pairwise peer keys let the device authenticate inbound secure traffic from those peers and send MAC acks on the host’s behalf while the host is asleep or disconnected, so that senders’ retransmission logic is satisfied.
  • Channel keys let the device recognize multicast traffic on the host’s channels—and, importantly, blind unicast traffic addressed to the host, whose destination and source addresses are concealed under the channel key (see Blind Unicast Packet). Without the channel key the device cannot even tell such traffic is for the host.

This does not grant the device the full power of the host’s identity. It only grants limited capability for the specific peers and channels that were provisioned. Provisioning Security specifies the exact mechanism and its security consequences.

Subsystems

The interface divides into nine subsystems, each specified by its own chapter and each discoverable through the capabilities it advertises. Only the first two are unconditional.

Radio Control

The host configures the physical radio link, through an interface that is not LoRa-specific in shape where that can be avoided—different radios have different parameter sets. The host can:

  • configure frequency, bandwidth, spreading factor, coding rate, power, and similar link parameters where applicable
  • query device capabilities and current active configuration
  • observe radio health and diagnostics
  • observe and limit transmit duty cycle

Frame Transport

The host uses the device as a transport for UMSH frames:

  • transmit of raw complete UMSH frames
  • receive of raw complete UMSH frames, with RSSI/SNR receive metadata
  • transmit result indications

This keeps the layering clean: the ULCP link carries UMSH frames, not re-encoded UMSH semantics. One practical consequence is that the host can also communicate with the device’s own local UMSH node using ordinary UMSH frames sent over this stream, rather than requiring a separate bespoke message path for such traffic.

Device Domain

The radio’s own node identity and the settings that make it behave the way its operator commissioned it: its keys and peers, its name, its repeater switch, what it advertises about itself on the mesh, and its battery telemetry. This is what a host writes when it is administering a device rather than being that device’s host.

Saved State

The device can snapshot its device-domain configuration to non-volatile storage and restore it at boot, which is what lets a radio come back after a power cut still configured and still forwarding with nobody attached.

Tethered Host Services

Receive filtering, inbound queueing, key provisioning, and acknowledgement delegation: the work a device does on behalf of the host identity it is serving.

A major value of a companion radio is letting the host sleep while the device stays awake. For that, the device filters—implicitly from the provisioned host identity and channel keys, explicitly by destination hint, channel identifier, or packet type—so it only wakes the host when a frame is relevant. While the host is disconnected it may also be asked to buffer inbound frames until the host asks for them, and to send MAC acks for peers whose pairwise keys were provisioned.

These remain tightly scoped: the device is assisting the host, not impersonating it in the general case. Outbound traffic is deliberately not queued—a transmit either happens or fails while the host is attached to observe the result.

Wi-Fi

The 802.11 hardware some devices carry alongside the LoRa radio, in three functions the host configures separately: scanning for access points, joining a network as a station, and offering one as an access point. Each is its own capability, because a tracker whose transceiver sniffs beacons for geolocation can scan and can do nothing else.

IP Connectivity

Addressing on whichever link the device has: DHCP or a static configuration per family, the addresses and routers in effect, and the resolvers. One capability per family, and nothing in it knows what the link is.

Bridge Client

The device’s own node connected to an internet bridge over IP: where to connect, which server key to trust, whether the client is enabled, and what state the connection is in.

IP Multicast

The device’s own node joined to an IP multicast group: whether it is enabled, which multicast group it joins, how many frames from the network it takes, and whether it has joined.

I2C Bus Access

Raw transactions with the device’s I2C peripherals, for bring-up and diagnosis: the buses and known peripherals the device names, one transaction per command with its read data in the reply, and a scan of who acknowledges. The device’s own drivers keep the bus between commands.

Suggested Capability Matrix

The table below summarizes which side owns which function.

CapabilityHostDevice
Long-term private identity keyYesNo
Fresh pairwise derivation for arbitrary new peersYesNo
Raw frame transmit / receiveOptionalYes
Radio parameter controlConfigureEnforce
Receive filteringConfigureEnforce
Channel keysYesOptional, provisioned
Pairwise keys for known peersYesOptional, provisioned
MAC acks for provisioned peers while host disconnectedConfigure/policyPerform
Inbound queueing while host absentDrain/consumePerform

Low-Power Expectations

A companion radio is especially useful when the host processor should remain asleep most of the time. The architecture supports:

  • the device remaining awake while the host sleeps
  • filtering, acknowledgement, and queueing happening on the device side
  • host wakeup only when relevant traffic arrives
  • reconnect and drain of queued frames without losing radio continuity

This fits well with the broader UMSH design goal that devices should wait on real events rather than spin in polling loops.

Protocol Shape

ULCP is a single transport-independent protocol, inspired by the framing discipline of Spinel but with a UMSH-specific command and property namespace. The same frames run over:

  • UART / USB-CDC serial, using HDLC-Lite framing
  • BLE, using the GATT frame transport of ULCP over BLE
  • a local socket between processes on one host, using HDLC-Lite framing
  • any other reliable, ordered, flow-controlled local transport

The key structural ideas:

  • lightweight binary framing with a one-byte header and small transaction identifiers, allowing up to seven in-flight host commands
  • properties for simple state—a change is confirmed by publication of the new authoritative value, and asynchronous state changes use the same publication form
  • streams for packet-like flows such as raw UMSH frames, which are not modeled as state
  • unsolicited notifications share the grammar of solicited responses

Framing and Common Semantics defines the wire format and the grammar every device implements; the subsystem chapters layer configuration and assistance features on top of it without changing the framing or the version.

Whatever the transport, ULCP is a privileged interface: an attached host commands transmission with arbitrary content, timing, and power, and provisioning moves real key material onto the device. On serial transports this is protected by physical possession, and on a local socket by access to the socket; the BLE binding specifies an equivalent barrier (see Security), and key provisioning must never be carried over a transport that provides less.

BLE As A Local Bearer

If BLE is used for more than tethering, it should be treated as a separate local bearer concept rather than an extension of ULCP.

Two broad BLE directions are relevant:

  • connection-oriented tethering, where one host talks directly to one radio over a local link
  • connectionless or mesh-style local participation, where multiple nearby devices can observe, relay, or respond

The first case is what ULCP is about. The second case is a different design problem.

For clarity:

  • tethered ULCP means “my host talks to my radio”
  • BLE local bearer means “nearby devices can discover and use this radio or exchange nearby UMSH-related traffic over BLE”

The first is point-to-point control and framing. The second is local network access.

Plausible BLE Building Blocks

BLE does have modes that are closer to local ad-hoc participation than ordinary GATT tethering:

  • ordinary LE advertising for one-to-many broadcast
  • periodic advertising for scheduled connectionless broadcast
  • Periodic Advertising with Responses (PAwR) for scheduled broadcast with slotted responses
  • Bluetooth Mesh, which defines an advertising bearer and a GATT bearer

These are the main reasons it is reasonable to think BLE could support a small local access or bridge protocol. In particular:

  • ordinary advertising can announce the presence, capabilities, and service class of a nearby radio
  • periodic advertising can provide a more structured broadcast schedule for status or downlink announcements
  • PAwR is notable because it adds scheduled responses, making it one of the clearer BLE building blocks for a low-rate shared local uplink/downlink model
  • Bluetooth Mesh is relevant less as a complete stack to adopt wholesale and more as proof that the Bluetooth ecosystem already recognizes both advertising-bearer and GATT-bearer styles of participation

Practical Payload Size Considerations

Not all BLE bearers are equally suitable for carrying complete UMSH frames.

For the tethered ULCP case, GATT is attractive partly because its payload sizes are large enough to be practical for whole-frame carriage. In BLE, an attribute value may be up to 512 octets, which in practice corresponds to the familiar “ATT MTU up to 517 bytes” figure once ATT overhead is included. That is comfortably in the range needed for ULCP.

Advertising-oriented bearers are different. Their payloads are much smaller, and they should therefore be treated as:

  • discovery bearers
  • short-message bearers
  • or fragmented local bearers

rather than assumed to be “GATT, but connectionless.”

As a practical rule of thumb:

BLE modeTypical role for UMSHPayload-size implications
GATTTethered host-to-device ULCP linkLarge enough for full ULCP frames and often full UMSH frames without special contortions
L2CAP CoCTethered host-to-device ULCP link where availableSimilar role to GATT, often a cleaner framing substrate
LE advertising / scan responseDiscovery, announcements, tiny local messagesSmall; should not be treated as a full-frame bearer without fragmentation
Periodic advertisingScheduled broadcast / downlink-style announcementsStill advertising-scale payloads; better for scheduled broadcast than general frame transport
PAwRScheduled low-rate local access with responsesMore interesting for shared local access, but still a constrained bearer compared with GATT
Bluetooth Mesh bearersSeparate larger design spacePotentially relevant architecturally, but implies adopting a much larger stack and message model

This suggests a clean split:

  • if the goal is host-to-device tethering, prefer GATT first and L2CAP CoC where available—this is what ULCP over BLE specifies
  • if the goal is nearby-device participation over BLE, assume the bearer is constrained and design for small messages or fragmentation from the outset

That BLE local bearer would need its own answers for questions such as:

  • how nearby devices discover an available bridge or repeater
  • how access is authorized
  • whether traffic is connectionless, connection-oriented, or mixed
  • whether the bearer only tunnels complete UMSH frames or also exposes local service messages
  • how buffering, fairness, and airtime limits are handled when several nearby clients share one radio

Bluetooth Mesh is particularly notable because the Bluetooth SIG already defines an advertising bearer and a GATT bearer, with Proxy nodes bridging between them. That architecture is conceptually similar to what a UMSH device may eventually want: one mode for direct tethered interaction and another for local many-to-many participation. At the same time, adopting Bluetooth Mesh itself would mean adopting a substantial stack, not just borrowing the bearer idea.

Open Questions

The following items remain intentionally open:

  • how much application-layer filtering is appropriate before violating layer separation
  • whether and how the device’s own node should announce itself
  • the design of a bridged / local-bearer mode, including whether bridged clients may provision keys or only use pre-provisioned shared services

Summary

A companion radio should be understood as a UMSH radio service with optional delegated capabilities, not as the default owner of the user’s identity. The host keeps authority over long-term identity, while the device contributes:

  • always-on physical connectivity
  • receive filtering and low-power wake support
  • inbound buffering and delegated acknowledgement for provisioned peers
  • optional narrowly scoped offline assistance

That split preserves UMSH’s cryptographic model while still making small, low-power, phone-connected radios practical.

ULCP: Framing and Common Semantics

This chapter defines the ULCP wire format and the semantics every device implements whatever else it supports: the frame layout, the command grammar, the property model, the classes of state a device holds, how a host attaches and synchronizes, and the numeric registries for status codes, reset codes, and capabilities. The subsystem chapters that follow build on it.

ULCP is inspired by the Spinel protocol from OpenThread, but it is not Spinel and does not aim for wire compatibility with it. The protocol assumes reliable, in-order delivery of frames, as well as a way to assert flow control. The framing mechanism depends on the underlying transport:

  • Asynchronous serial links (UART, USB-CDC) use HDLC-Lite, exactly as used by Spinel.
  • BLE uses the GATT frame transport defined in ULCP over BLE.
  • A reliable, in-order byte stream standing in for a serial link—a TCP connection to a bridged port, say—uses the same HDLC-Lite framing.
  • A local stream socket between two processes on one host—a Unix domain socket—uses HDLC-Lite as well; see Local Sockets.

In this chapter, the device is the side that owns the transceiver and the host is the side that attaches to it over the local link (see Local Control Protocol).

The protocol version is 6.0. Which subsystems a device implements is discovered through PROP_CAPS, never through the version number; see Minimum Requirements for what a device is required to implement in order to be a ULCP device at all.

Data Representation

Spinel, being a low-level protocol between two devices which are likely to have a little-endian architecture, uses little-endian representations exclusively for all integers smaller than four bytes. For implementation convenience, values larger than four bytes (EUI64, IPv6 addresses, etc.) are stored as they are traditionally represented (typically, but not always, big-endian).

Packed Unsigned Integers

Certain types of integers, such as command or property identifiers, usually have a value on the wire that is less than 127. However, in order to not preclude the use of values larger than 255, we would need to add an extra byte. Doing this would add an extra byte to all packets, which can add up in terms of bandwidth. To address this, Spinel uses Packed Unsigned Integers, or PUIs.

The PUI format used in Spinel is based on the unsigned integer format in EXI, except that we limit the maximum value to the largest value that can be encoded in three bytes. The maximum value that can be encoded is 2,097,151.

For all values less than 127, the packed form of the number is simply a single byte which directly represents the number. For values larger than 127, the following process is used to encode the value:

  1. The unsigned integer is broken up into n 7-bit chunks and placed into n bytes, leaving the most significant bit of each byte unused.
  2. Order the bytes from least-significant to most-significant. (Little-endian)
  3. Clear the most significant bit of the most significant byte. Set the most significant bit on all other bytes.

Where n is the smallest number of 7-bit chunks you can use to represent the given value.

Take the value 1337, for example:

1337 => 0x0539
     => [39 0A]
     => [B9 0A]

To decode the value, you collect the 7-bit chunks until you find a byte with the most significant bit clear.

Frame Format

A ULCP frame is the concatenation of the following elements:

  • A header comprising a single byte.
  • A command identifier.
  • A command-defined payload, which may be empty.
  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     HEADER    |  COMMAND ID   | PAYLOAD ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of a typical ULCP frame

Since the size of the frame is part of the framing mechanism, it is omitted from the frame.

Frame Header

Each frame has the following format:

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
|  FLG  | RESERVED  |    TID    |
+---+---+---+---+---+---+---+---+

Figure: Header Format

FLG: Flag

The Flag (FLG) field in the two most significant bits of the header byte is always set to the value two (or 10 in binary). Any frame received with these bits set to any other value SHALL NOT be considered a ULCP frame.

RESERVED: Reserved

These three bits must always be set to zero and the entire frame ignored if set to any other value. They may be assigned a meaning (such as an interface identifier) in a future version of this protocol.

TID: Transaction Identifier

The Transaction Identifier (TID) field in the three least significant bits of the header is used for correlating responses to the commands which generated them. This allows for up to seven host-issued commands to be in flight at once.

When a command is sent from the host, any reply to that command sent by the device will use the same value for the TID. When the host receives a frame that matches the TID of the command it sent, it can easily recognize that frame as the actual response to that command.

The zero value of TID is used for commands to which a correlated response is not expected or needed, such as for unsolicited update commands sent to the host from the device.

Note that while the frame format is symmetric between the frames being sent to the device versus frames being sent from the device, the behaviors are not. The device MUST NOT send a frame with a non-zero TID that is not a response to a frame it had recently received with that same TID. All unsolicited or asynchronous commands originating from the device MUST use TID zero (0).

Command ID

The command identifier is a 7-bit unsigned integer encoded from 0 to 127. The most significant bit is not set and the frame must be ignored if it is set.

Payload

The command payload follows the command identifier in a ULCP frame, containing the serialization of any arguments that the indicated command may require. The exact composition of a command payload is determined by the specific command identifier being used and MUST be empty if the command has no arguments.

Commands

This chapter defines the commands that operate on the protocol itself— resets, liveness, and the property grammar. The remaining commands are defined with the subsystem they act on: CMD_STR_SEND and CMD_STR_RECV in Frame Transport, CMD_QUEUE_DRAIN in Tethered Host Services, CMD_ANNOUNCE in Device Identity, the four state-management commands in Saved State, and the three bus-access commands in I2C Bus Access. The complete numeric allocation is in the Command and Property Index.

IdMnemonicDirDescription
0CMD_NOPHost->DeviceNo-Operation
1CMD_RSTHost->DeviceReset the device
2CMD_PROP_GETHost->DeviceGet property value
3CMD_PROP_SETHost->DeviceSet property value
4CMD_PROP_INSERTHost->DeviceInsert an item into a multi-value property
5CMD_PROP_REMOVEHost->DeviceRemove an item from a multi-value property
6CMD_PROP_ISDevice->HostProperty value notification
7CMD_PROP_INSERTEDDevice->HostItem-inserted notification
8CMD_PROP_REMOVEDDevice->HostItem-removed notification
16CMD_REBOOTHost->DeviceRestart the device’s hardware
17CMD_DFUHost->DeviceEnter bootloader firmware-update mode
19CMD_ANNOUNCEHost->DeviceAnnounce the device now
21CMD_PROP_MULTI_GETHost->DeviceGet several property values
22CMD_PROP_MULTI_SETHost->DeviceSet several property values in order
23CMD_PROP_AREDevice->HostMultiple property value notification
24CMD_SESSION_RESETDevice->HostSession state was discarded
25CMD_I2C_TRANSFERHost->DeviceOne I2C transaction
26CMD_I2C_RESULTDevice->HostRead data or scanned addresses
27CMD_I2C_SCANHost->DeviceProbe a range of I2C addresses

The multi-property commands (21–23) are gated by CAP_CMD_MULTI, CMD_REBOOT, CMD_DFU, and CMD_FACTORY_RESET by CAP_REBOOT, CMD_ANNOUNCE by CAP_ADVERT, and the bus-access commands (25–27) by CAP_I2C (see Capabilities); everything else is unconditional.

CMD 0: (Host -> Device) CMD_NOP

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_NOP    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
^     HEADER    ^    COMMAND    ^

Figure: Structure of CMD_NOP

No-Operation. Commands the device to reply with a STATUS_OK code. This is primarily used for liveness checks.

The command payload for this command SHOULD be empty. The receiver MUST ignore any non-empty command payload.

There is no error condition for this command.

CMD 1: (Host -> Device) CMD_RST

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_RST    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_RST

Reset device. Commands the device to perform a software reset. Due to the nature of this command, the TID is ignored. The host should instead wait for a CMD_PROP_IS command from the device indicating PROP_LAST_STATUS has been set to STATUS_RESET_SOFTWARE (see Status Codes).

The command payload SHOULD be empty, and it SHOULD NOT be processed.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 2: (Host -> Device) CMD_PROP_GET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_GET

Get property value. Commands the device to emit a CMD_PROP_IS command for the given property identifier.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 3: (Host -> Device) CMD_PROP_SET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  NEW PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_SET

Set property value. Commands the device to set the given property to the specific given value, replacing any previous value, and to emit a CMD_PROP_IS command for that property indicating the new authoritative value if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers, followed by the property value. The exact format of the property value is defined by the property.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

The value reported by that CMD_PROP_IS need not be the value written: a device that adjusts a write to what it can honor reports the result, and that result is the property’s value. A write fails only by way of PROP_LAST_STATUS—a differing value is a successful write, not a rejected one.

CMD 4: (Host -> Device) CMD_PROP_INSERT

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ITEM VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_INSERT

Insert item into property. Commands the device to add the given item to the given multi-value property, and to emit a CMD_PROP_INSERTED command for that property if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by exactly one item encoded in the property’s item form (see Multi-Value Properties). The item is not preceded by a length prefix, regardless of whether the property uses item length prefixes in its multi-item value form; the framing layer bounds the item.

If the item is already present the command fails with STATUS_ALREADY, except where a property defines replacement semantics for matching items (see, e.g., PROP_HOST_PEER_KEYS). If the property exists but is not a multi-value property, the command fails with STATUS_INVALID_ARGUMENT.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 5: (Host -> Device) CMD_PROP_REMOVE

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ITEM SELECTOR ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_REMOVE

Remove item from property. Commands the device to remove the item matching the given selector from the given multi-value property, and to emit a CMD_PROP_REMOVED command for that property if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by an item selector. Each multi-value property documents its selector form; unless stated otherwise it is the full item value.

If no matching item is present, the command fails with STATUS_ITEM_NOT_FOUND. If the property exists but is not a multi-value property, the command fails with STATUS_INVALID_ARGUMENT.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 6: (Device -> Host) CMD_PROP_IS

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  CURRENT PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_IS

Property value notification. This command can be sent by the device in response to a previous command from the host, or it can be sent by the device in an unsolicited fashion to notify the host of various state changes asynchronously.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers, followed by the current value of the given property.

CMD 7: (Device -> Host) CMD_PROP_INSERTED

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_INSERTED

Item-inserted notification. Sent by the device in response to a successful CMD_PROP_INSERT (with the TID of that command), or unsolicited with a TID of zero when the device adds an item to a multi-value property for its own reasons.

The payload is the property identifier followed by the inserted item as the device reports it (see Multi-Value Properties)—never in a form containing key material.

CMD 8: (Device -> Host) CMD_PROP_REMOVED

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_REMOVED

Item-removed notification. Sent by the device in response to a successful CMD_PROP_REMOVE (with the TID of that command), or unsolicited with a TID of zero when the device removes an item from a multi-value property for its own reasons.

The payload is the property identifier followed by the removed item as the device reports it.

CMD 16: (Host -> Device) CMD_REBOOT

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |  CMD_REBOOT   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_REBOOT

Restart the device. Commands the device to power-cycle its hardware, keeping every piece of state it has persisted: the saved snapshot, the device identity, the pairing PIN, and all bonds survive, and the device comes back configured as it was.

This differs from CMD_RST, which returns protocol state to its post-reset values with the device still running, and from CMD_FACTORY_RESET, which erases that state before restarting. Between the three, this is the one that changes nothing—it is how a host clears a condition the protocol cannot name.

The command payload SHOULD be empty and MUST be ignored. A device that restarts sends no response: the reboot drops the transport link, and the TID is therefore irrelevant. A host treats the ensuing disconnect (and the device’s subsequent reappearance announcing STATUS_RESET_POWER_ON) as completion, and MUST NOT wait for a PROP_LAST_STATUS.

This command is only available on devices advertising CAP_REBOOT. A device without it answers STATUS_UNIMPLEMENTED—which is the only response this command ever produces, and the only thing that distinguishes a device that declined from one that is already restarting.

CMD 17: (Host -> Device) CMD_DFU

Enter the platform’s bootloader for a firmware update. The payload is empty or one UINT8 selecting the DFU method:

ValueMethod
0Default DFU
1Serial/USB-CDC DFU
2UF2 (file-copy) DFU
3BLE DFU

An empty payload selects Default. The default is fixed by the platform, independent of the command’s transport. XIAO nRF52 and SenseCAP Solar use BLE; T-Echo, T1000-E, and Wio Tracker L1 use UF2. Other mode values are undefined. Entering the bootloader is a restart into it, so this command is only available on devices advertising CAP_REBOOT; a device without it answers STATUS_UNIMPLEMENTED. There is no discovery property for the methods; the device decides whether its supported bootloader can enter the requested mode.

The device answers with CMD_PROP_IS(PROP_LAST_STATUS):

StatusMeaning
STATUS_OKPrepared to enter DFU after transmitting this response
STATUS_UNIMPLEMENTEDThe platform or its supported bootloader cannot enter this method
STATUS_INVALID_ARGUMENTThe mode is undefined
STATUS_PARSE_ERRORThe payload contains more than one byte

Other failure statuses report preparation failures and leave the application running. Before returning success, the device MUST complete all fallible preparation, including persisting replay-counter state. It MUST NOT enter DFU until the complete matching response has transmitted. Queue acceptance alone is insufficient. Failed, canceled, or expired response transmission MUST leave the application running and invalidate the pending action and any retained success response. Completion of another response, or of an earlier transport generation, MUST NOT authorize entry.

A direct request MUST use a nonzero TID. A direct TID-zero request records STATUS_INVALID_ARGUMENT without entering DFU. Over Node Management, the TID is zero and the ordinary token-correlated response confirms acceptance. This command does not use reset-command acknowledgment semantics.

DFU entry preserves saved configuration, identity, bonds, and pairing credentials. Unsaved application state is lost. Existing administrative authentication and authorization apply. Bootloader residency and exit behavior are bootloader-defined; automatic return to UMSH is not promised. The command enters DFU only and does not transfer or install firmware.

A host MUST report success only upon receiving STATUS_OK. Timeout or disconnect before that response leaves entry unconfirmed and MUST NOT cause an automatic new DFU command. Hosts SHOULD warn that the device may remain inaccessible indefinitely, and that BLE updating requires an updater within Bluetooth range. Older firmware’s STATUS_INVALID_COMMAND is treated as unsupported. This additive command does not change the protocol version.

CMD 21: (Host -> Device) CMD_PROP_MULTI_GET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI) | PROP_KEY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_MULTI_GET

Get several property values at once. Commands the device to reply with a single CMD_PROP_ARE carrying one entry for each requested property, in request order.

The payload is one or more property identifiers, each encoded in the packed unsigned integer format, one after another with no delimiters.

Fetching continues past failures: a property that cannot be fetched occupies its position in the reply as a PROP_LAST_STATUS entry whose value is the status code a CMD_PROP_GET of that property would have produced. Position, not the entry’s key, identifies which request an entry answers.

This command is available only on devices advertising CAP_CMD_MULTI. On any other device it is an unrecognized command, STATUS_INVALID_COMMAND.

Battery diagnostics define a shared acquisition group: when a device implements those properties, supported battery readings within one multi-get use one acquisition pass, including duplicate keys. This does not make other properties atomic or imply simultaneous hardware measurements.

CMD 22: (Host -> Device) CMD_PROP_MULTI_SET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | ENTRY | ENTRY | ENTRY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_MULTI_SET

Set several property values in order. The payload is a sequence of entries, each the combined length of its key and value encoded as a packed unsigned integer, followed by that many octets—the property identifier as a packed unsigned integer, then the value:

+--------------+---------------------------+--------------------+
| LENGTH (PUI) | PROP_KEY (PUI, 1-3 bytes) | VALUE (remainder)  |
+--------------+---------------------------+--------------------+

Figure: CMD_PROP_MULTI_SET Entry Format

The device applies each entry exactly as a CMD_PROP_SET of that property would, strictly in payload order, stopping at the first entry that fails. Mutation Atomicity applies to each entry alone: the sequence is not a transaction, and a failure partway leaves the earlier entries applied.

The reply is a single CMD_PROP_ARE containing, for each applied entry in order, the property and its reported value— exactly what the CMD_PROP_IS answering a lone CMD_PROP_SET would carry—and, for the failing entry, a PROP_LAST_STATUS entry carrying the error, after which the reply ends. Entries past the failure are not executed and contribute nothing; a reply whose every entry is a success covers the entire request.

This command is available only on devices advertising CAP_CMD_MULTI. On any other device it is an unrecognized command, STATUS_INVALID_COMMAND.

CMD 23: (Device -> Host) CMD_PROP_ARE

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | ENTRY | ENTRY | ENTRY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_ARE

Multiple property value notification. The payload is a sequence of entries in the same encoding as CMD_PROP_MULTI_SET: a combined key-and-value length as a packed unsigned integer, the property identifier, and the value as the device reports it—under the same reporting rules as CMD_PROP_IS, so key material never appears (see Multi-Value Properties).

The device emits this command only in response to CMD_PROP_MULTI_GET or CMD_PROP_MULTI_SET, with the TID of that command. It MUST NOT be emitted unsolicited: asynchronous updates use CMD_PROP_IS and its companions, one property at a time.

CMD 24: (Device -> Host) CMD_SESSION_RESET

  0                   1                   2
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      |     REASON
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_SESSION_RESET

The device has discarded session state, so every session-scoped property is back at its documented default under a host that did not ask for it. The TID MUST be zero: the frame is always unsolicited.

REASON is a packed unsigned integer. It is diagnostic—a host’s obligation is the same for every value:

ValueMeaning
0A host attached
1CMD_RST
2CMD_RESTORE in its reset form

Unassigned values are reserved. A host MUST treat an unrecognized reason as a session reset it does not have a name for, never as a parse failure.

A device MUST emit this command whenever it discards session state while a host is attached, and MUST NOT emit it when no host is attached: a detach discards session state with nobody to tell.

The frame MUST be emitted before any frame belonging to the new session, so a host can tell which session anything it receives came from. The completion of the command that caused the discard belongs to the exchange that requested it and MAY precede the notice; a CMD_RST is answered by its STATUS_RESET_SOFTWARE and then the notice. Where a session is instead created by the host’s own first frame, nothing has been answered yet and the notice comes first.

The command MUST NOT be emitted over the administrative binding. It concerns the local tethered session; an administrator over the mesh is not party to it, and the binding carries only what was asked for.

A host MUST tolerate the frame at any time while attached. On receiving it, a host MUST re-establish anything it holds in session state and SHOULD otherwise resynchronize as it would on a fresh attach (see Attach, Detach, and Synchronization).

This is deliberately not a reset code, and MUST NOT disturb PROP_LAST_STATUS. The two report different losses: STATUS_RESET_POWER_ON means the host domain went with the session and must be reprovisioned in full, whereas a session reset means only session state went. A reboot implies a session reset, so recording the narrower fact over the broader one would lose the distinction that governs recovery.

Properties and Streams

A property is a piece of device state with a value the host can read and, where the property allows it, write. A stream is a packet-like flow that is not modeled as state; streams share the property identifier space and are carried by their own commands (see Frame Transport).

Note

The properties marked as supporting Is means that the property may be emitted asynchronously. All properties that support Get or Set will emit an Is to respond with the current/new value of that property.

A multi-value property marked as supporting Inserted or Removed may likewise emit those asynchronously, reporting one item the device added or dropped for its own reasons rather than the whole value. A host that tolerates an unsolicited Is must tolerate these as well.

Multi-Value Properties

A multi-value property holds an unordered set of items rather than a single value. PROP_CAPS is one, and is constant; the key, peer, and filter tables of the device and host domains are mutable ones.

The host writes items (CMD_PROP_SET, CMD_PROP_INSERT) in the property’s item form. When the device reports items (CMD_PROP_IS, CMD_PROP_INSERTED, CMD_PROP_REMOVED), it reports them exactly as written—except where the item form contains symmetric key material. Such a property documents its reported form instead: the entry with its key material omitted, or a value derived from it (a channel key is reported as its derived channel identifier), so that secrets can never be read back (see Provisioning Security).

The commands valid on a mutable multi-value property are:

  • CMD_PROP_GET—the device replies with CMD_PROP_IS whose value is the concatenation of all items as reported. If the property is documented as having an item length prefix, each item is preceded by its length in octets encoded as a packed unsigned integer; properties whose reported items are fixed-size omit the prefix.
  • CMD_PROP_SET—replaces the entire contents with the items encoded in the value, each in item form (with the same length-prefix rule). Setting an empty value clears the property. Success is reported with a CMD_PROP_IS carrying the new complete value as reported.
  • CMD_PROP_INSERT / CMD_PROP_REMOVE—add or remove one item, as defined above.

Hosts manipulating large tables SHOULD prefer Insert/Remove over whole-table Set, since a full table may not fit comfortably in one frame on all transports.

Mutation Atomicity

State-changing operations in this protocol are transactional and fail closed:

  • The device MUST validate a complete request before changing any state. A whole-table CMD_PROP_SET whose value contains any invalid item fails without applying any of it.
  • Whole-table replacement is atomic: no observer of device behavior (frame filtering, acknowledgement decisions) sees a mixture of the old and new contents.
  • Operations that include durable writes—CMD_SAVE, CMD_CLEAR, installing or generating the device identity, and setting PROP_BLE_PAIRING_PIN—MUST NOT report success before the durable write has completed.
  • On any failure, the prior live and durable state remains unchanged, and the device MUST NOT emit CMD_PROP_IS, CMD_PROP_INSERTED, or CMD_PROP_REMOVED notifications describing a partially applied change.
  • Host replacement is atomic in the same sense: at no point may the device operate with a mixture of the old and new hosts’ keys, filters, or policy. It involves no durable write, so it cannot fail partway.

Atomicity is per operation, not per sequence. Establishing a host domain is several property writes, and an interruption between them leaves a mixture of old and new—bounded by the fact that a host-key change resets the domain first and a reboot empties it. A host repairs this the same way it provisions in the first place: by writing everything again.

Core Properties

These properties exist on every device and are not gated by any capability.

IdMnemonicCommandsDescription
0PROP_LAST_STATUSGet, IsLast status
1PROP_PROTOCOL_VERSIONGetProtocol version
2PROP_DEV_VERSIONGetDevice version string
3PROP_INTERFACE_TYPEGetInterface type
5PROP_CAPSGetCapabilities

PROP 0: PROP_LAST_STATUS

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: REQUIRED
  • Value Type: PUI + STRING(opt.)
  • Units: Enumeration
  • Post-Reset Value: Reset Reason Code

This property describes the status code of the last device operation. For many commands, failure is indicated by emitting CMD_PROP_IS for this property with a TID matching the failing command. It is generally not necessary to ever fetch the value of this property explicitly, as it is often emitted directly as an error response. It is also occasionally emitted as a success response with a value of STATUS_OK.

Upon device reset, this property MUST be emitted with a status code indicating the reset reason.

Upon receiving an asynchronous update to PROP_LAST_STATUS with a status code that indicates a reset, the host SHALL assume that the device has been reset and that all properties have reverted to their defined after-reset values.

See Status Codes for the complete list of status codes.

PROP 1: PROP_PROTOCOL_VERSION

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: Device
  • Value Type: UINT8, UINT8
  • Post-Reset Value: 6, 0

Describes the ULCP version information. This property contains two fields:

  0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MAJOR_VERSION | MINOR_VERSION |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: PROP_PROTOCOL_VERSION Value Format

MAJOR_VERSION
The major version number is used to identify backward incompatible differences between protocol versions.
MINOR_VERSION
The minor version number is used to identify backward-compatible differences between protocol versions. A mismatch between the advertised minor version number and the minor version that is supported by the host SHOULD NOT be fatal to the operation of the host.

This document describes major version 6, minor version 0 of this protocol.

PROP 2: PROP_DEV_VERSION

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: Device
  • Value Type: STRING
  • Post-Reset Value: Implementation-Specific

Contains a zero-terminated ASCII string which describes the firmware currently running on the device.

The value of this string MUST be different for every firmware release.

The format of the string is not strictly defined, but it is intended to present similarly to the “User-Agent” string from HTTP. The following format is RECOMMENDED:

STACK-NAME/STACK-VERSION[BUILD-INFO][; OTHER-INFO][; BUILD-DATE]

The hardware the firmware is running on belongs in PROP_DEV_MODEL, not here.

PROP 3: PROP_INTERFACE_TYPE

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: PUI
  • Units: Enumeration
  • Post-Reset Value: Implementation-Specific

This unsigned packed integer identifies the network protocol implemented by this device. It must return the value 8.

PROP 4: PROP_DEV_MODEL

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: OPTIONAL
  • Scope: Device
  • Value Type: STRING
  • Post-Reset Value: Implementation-Specific

Contains a zero-terminated ASCII string naming the hardware model the device is, such as Seeed SenseCAP T1000-E. Where PROP_DEV_VERSION describes the firmware a device runs, this describes the thing it runs on, and the two change independently: the same firmware release covers several models, and a model outlives every release built for it.

Firmware built for one specific board SHOULD implement this. A device whose hardware has no fixed identity—a simulator, or an implementation that runs on whatever it is compiled for—SHOULD omit the property rather than return an empty or invented string. A host MUST treat a refused get as “this device does not name its hardware” and continue.

The value SHOULD name the product as its vendor does, so that a host can match it against an external hardware description. It is intended for display and for lookup, and is deliberately not an identifier: a host that needs to make decisions based on what a device can do has PROP_CAPS for that.

PROP 5: PROP_CAPS

  • Type: Multiple-Value, Constant
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Item Type: PUI
  • Units: Enumeration
  • Post-Reset Value: Implementation-Specific

Describes the supported capabilities of this device. Encoded as a list of packed unsigned integers. See Capabilities for a list of values.

PROP 6: PROP_UPTIME

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: OPTIONAL
  • Scope: Device
  • Value Type: UINT32
  • Units: Seconds
  • Post-Reset Value: 0

Seconds elapsed since the device last booted, truncated toward zero. A device that runs long enough to exhaust the range saturates rather than wrapping, so the value never falsely reports a recent restart.

This dates what PROP_LAST_STATUS describes: the reset code says why the device last started, and this says how long ago. Read together they distinguish a node that came up cleanly weeks back from one that is restarting under a fault and reporting the same code each time.

Only a power cycle or a genuine restart resets it. CMD_RST returns protocol state to its post-reset values without rebooting the device, and MUST NOT reset this property; neither does a host attaching or detaching.

A device with no monotonic clock to answer from SHOULD omit the property rather than report a fabricated value. A host MUST treat a refused get as “this device does not report its uptime” and continue.

State Classes

Every piece of device state belongs to exactly one of three classes. The classes determine what survives a host attach, a change of host, and a power cycle.

Session State

State that exists only while a host is attached: transaction (TID) correlation, transport reassembly buffers, and the session-scoped properties PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL. Session state is reset to defaults on every attach. Resetting it never affects radio operation.

Device Domain

State that belongs to the device itself, independent of which host is attached:

  • the device identity keypair (independently persisted; never part of the saved snapshot—see Saved State)
  • the device identity’s channel keys (PROP_DEV_CHANNEL_KEYS) and peer list (PROP_DEV_PEERS)
  • the RF configuration (PROP_PHY_*), including PROP_PHY_ENABLED, and the duty-cycle limit
  • the human-readable device name (PROP_DEV_NAME)
  • live battery telemetry (PROP_BATTERY), when CAP_BATTERY is present
  • device behavior settings (property identifiers 70–95): the repeater forwarding switch (PROP_MAC_REPEATER_ENABLED), with the rest of the range reserved for future definition (further repeater policy, positioning, periodic advertisement of the device identity, and similar)
  • the administrator list (PROP_DEV_ADMINS) authorizing node management over the mesh
  • transport configuration such as PROP_BLE_PAIRING_PIN

The RF configuration is deliberately device-domain: a site repeater keeps its frequency and regulatory limits no matter which phone pairs with it. An attached host may still reconfigure it at any time.

Host Domain

State that belongs to the currently configured tethered host identity:

  • PROP_HOST_KEY itself
  • the host’s channel keys and peer keys
  • the receive filter table and acknowledgement-delegation policy
  • the inbound queue: its configuration and its contents

The host domain is volatile across a power cycle, and only across a power cycle. It MUST NOT be persisted: it is not part of the saved snapshot, and at power-on every host-domain property takes its documented default.

It emphatically does survive a disconnect. A detached radio keeps filtering, queueing and acknowledging on behalf of its host for as long as it stays powered—that is the entire value of the host domain, and nothing about the host going out of range changes what the host wants done.

The two together give the host a simple rule with no detection in it: a host MUST establish its complete host domain on every tethered attach, writing every part of it rather than reasoning about what the device already holds. Key material cannot be compared anyway—the key tables never read it back (see Provisioning Security), so a peer’s pairwise keys can be replaced without changing anything the host can observe. Where the device is already provisioned as asked, the rewrite is redundant; that is preferable to depending on a signal that would also have to cover partial provisioning, another administrator’s intervention, and future device behavior. A boot generation or reset indication MAY be used to skip the rewrite as an optimization, never to decide whether it is needed.

Property Allocation

Property and stream identifiers are allocated by state class and subsystem:

RangeClass
0–31Core protocol state
32–47Radio control (PROP_PHY_*)
48–63Session-scoped and global protocol state
64–95Device domain
96–111Host domain (PROP_HOST_*)
112–127Streams (STR_*)
4608–4863Extended radio control
4864–5119Extended device and transport configuration

Unassigned identifiers in these ranges are reserved. The identifiers in use are listed in the Command and Property Index.

Attach, Detach, and Synchronization

How attach and detach are detected is defined by the transport binding:

  • BLE—enabling/disabling notifications on Frame Out, as specified in ULCP over BLE.
  • USB-CDC—assertion and deassertion of DTR on the ULCP interface.
  • TCP—establishment and closure of the connection.
  • Local socket—establishment and closure of the connection.
  • Bare UART—implementation-defined. A device with no way to detect host presence MAY treat the host as permanently attached, in which case it never enters detached operation and offline assistance (Inbound Queueing, Acknowledgement Delegation) is unavailable on that transport.

On attach, the device MUST reset session state (see State Classes) and MUST NOT modify the device or host domains in any way. In particular, the PHY is not disabled and no property outside session state changes value. The device MUST NOT emit any frame before attach; the first frame of the session is the CMD_SESSION_RESET announcing the session state the attach discarded, with reason 0, and the attach itself produces no other unsolicited notification.

Because attach no longer implies any known default state, the host synchronizes by fetching, not by assuming. The following post-attach procedure is RECOMMENDED:

  1. CMD_PROP_GET for PROP_LAST_STATUS. If it returns a reset code (see Reset Codes), the device has reset since the last host command, so any state that is not restored from saved state (notably queue contents) has been lost.
  2. CMD_PROP_GET for PROP_HOST_KEY. An empty value is the ordinary case after a power cycle—the host domain does not survive one—and the host simply provisions. A value matching the host’s own identity means its provisioning is still live from before the disconnect. Any other value means another host has taken the radio over since this host last attached; the queue and provisioning belong to that identity, and this host must decide whether to take the radio over (see Host Replacement) before doing anything else.
  3. CMD_PROP_GET for the device-domain properties the host depends on (PROP_SAVED, the PROP_PHY_* configuration), and for PROP_HOST_RX_QUEUE_COUNT.
  4. Establish the complete host domain (see Host Domain): write PROP_HOST_KEY, the key tables, the filter table and the delegation policy in full. The key tables are read back only to find entries the host no longer wants, which it removes; entries it does want are written whether or not the device reports them, since key material is not readable and so cannot be compared.
  5. Issue CMD_QUEUE_DRAIN when actually ready to process backlogged traffic.

More generally, a host MUST tolerate unsolicited CMD_PROP_IS, CMD_PROP_INSERTED, and CMD_PROP_REMOVED notifications at any time while attached, updating its view of the affected property accordingly: device state can change for reasons the host did not initiate, and publication of the new authoritative value is how the protocol reports that.

A request from another binding is one such reason. Whatever a request changes, the device MUST publish to everyone else listening—an attached host, and the device’s own interface— whether the request came from one of them or from a Node Management administrator. Each property the answer reports goes out unsolicited, one at a time, as the answer reported it; the status that closes the request goes to the requester alone. This holds for every property: a property’s Asynchronous Updates entry describes the changes the device makes for its own reasons. A fire-and-forget request (TID 0) on a local binding is not answered, and what it changes need not be published.

The same applies to CMD_SESSION_RESET, which reports the one change no property notification can: the session itself starting over. A host that receives one runs this procedure again.

On detach, the device discards session state, keeps operating with the current device- and host-domain state, and begins detached operation: accepted frames are queued rather than delivered, and acknowledgement delegation (if enabled) becomes active.

Local Sockets

A device that runs as a process—a virtual radio hosted beside the application that attaches to it—may serve ULCP on a local stream socket, such as a Unix domain socket:

  • Framing: HDLC-Lite, as on a serial link.
  • Attach and detach: a connection opening is an attach, and its closing is a detach.
  • One host at a time: a new connection replaces the current one, which the device closes first. A host that went away without closing never holds the device against its successor.
  • Security binding: access to the socket. The device MUST place the socket where the operating system lets only the processes entitled to the device reach it—a file in a directory private to those processes—which stands in for physical possession.

The Device’s Own Interface

A device with a settings interface of its own—a screen and keyboard, or a menu—may administer its own session in process, with the commands a host uses. The interface is a binding of its own:

  • Authority. Whoever is holding the device has physical possession, which is a serial link’s security binding (see Provisioning Security). The interface may provision key material, the device identity’s private key included.
  • Reach. The device domain. Session state, the host domain, the raw PHY stream, and the receive queue belong to whichever host is tethered, and the device answers the interface’s writes to them with STATUS_NOT_PERMITTED, as it answers an administrator’s.
  • A local binding. The device answers every request that carries a non-zero TID exactly once.
  • Not an attach. Its requests reset no session state, emit no CMD_SESSION_RESET, and displace no attached host.
  • Publications. The interface hears everything the device publishes, whether or not a host is attached, including what an attached host or an administrator changes. What the interface changes reaches an attached host the same way (see Attach, Detach, and Synchronization).

What the interface changes is saved as anything set at the device is (see Changes Made at the Device).

Provisioning Security

Provisioning moves real key material onto the device, within the limits of the security boundary: channel keys and per-peer symmetric keys—and the device identity’s own private key— but never the host’s private key. The rules:

  • All symmetric key material, and the device identity private key, is write-only. CMD_PROP_GET and all device-emitted notifications report key-bearing properties without their secrets (see Multi-Value Properties): peer public keys without K_ENC/K_MIC, derived channel identifiers instead of channel keys, and never the device private key. This holds for both identities’ key tables. These read-backs let the host verify what is provisioned after a reconnect without any secret ever crossing the link a second time— which matters because more than one host may be able to attach over the radio’s lifetime (transport bonds are possession credentials, not identity credentials), and a later host must not be able to extract an earlier host’s keys.
  • Commands that carry key material—CMD_PROP_SET and CMD_PROP_INSERT for the key tables, and any set of PROP_DEV_PRIVATE_KEY—MUST NOT be carried over a transport that does not meet the requirements of the transport’s security binding: physical possession for serial transports, access to the socket for a local socket, or an encrypted bonded LESC link as specified in ULCP over BLE.
  • A compromised or stolen device exposes the provisioned channels, the provisioned pairwise conversations, and its own device identity, but cannot impersonate the host to any new peer, cannot sign as the host, and cannot decrypt traffic for peers or channels that were never provisioned.
  • Hosts SHOULD provision the minimum useful set of peers and channels, SHOULD remove entries that are no longer needed, and SHOULD prefer on-device generation of the device identity over installing one.
  • A device advertising CAP_SAVE MUST store persisted key material in the most protected storage available to it.

Status Codes

Status codes are used for PROP_LAST_STATUS. When a command generates a status code, it is returned via a CMD_PROP_IS with a property of PROP_LAST_STATUS and the TID of command it is referring to.

IdName
0STATUS_OK
1STATUS_FAILURE
2STATUS_UNIMPLEMENTED
3STATUS_INVALID_ARGUMENT
4STATUS_INVALID_STATE
5STATUS_INVALID_COMMAND
7STATUS_INTERNAL_ERROR
9STATUS_PARSE_ERROR
10STATUS_IN_PROGRESS
11STATUS_NOMEM
12STATUS_BUSY
13STATUS_PROP_NOT_FOUND
18STATUS_CCA_FAILURE
19STATUS_ALREADY
20STATUS_ITEM_NOT_FOUND
21STATUS_CURSOR_INVALID
22STATUS_NOT_PERMITTED
23STATUS_CHANNEL_NOT_FOUND
24STATUS_NO_DEVICE
25STATUS_NACK
26STATUS_BUS_ERROR
32STATUS_DUTY_LIMIT
STATUS_OK
Indicates that the operation has completed successfully.
STATUS_FAILURE
Indicates that the operation has failed for an unspecified reason. The use of this status code SHOULD be avoided. If a more specific status code exists that better explains the failure, then that status code MUST be used instead.
STATUS_UNIMPLEMENTED
Indicates that the given operation has not been implemented.
STATUS_INVALID_ARGUMENT
Indicates that an argument to the given operation is invalid. The value may be out of range or improperly formatted. This status code is also returned when setting an invalid value to a property.
STATUS_INVALID_STATE
Indicates that the given operation is invalid for the current state of the device.
STATUS_INVALID_COMMAND
The given command id is not recognized.
STATUS_INTERNAL_ERROR
An internal runtime error has occurred.
STATUS_PARSE_ERROR
An error has occurred while parsing the command.
STATUS_IN_PROGRESS
Indicates that the operation was started but has not completed, and completion will be reported asynchronously.
STATUS_NOMEM
The operation has been prevented due to memory pressure.
STATUS_BUSY
The device is currently performing a mutually exclusive operation. This status differs from STATUS_INVALID_STATE in that it will resolve spontaneously.
STATUS_PROP_NOT_FOUND
The given property key is not recognized.
STATUS_ALREADY
The requested state is already in effect; in particular, the item passed to CMD_PROP_INSERT is already present in the property.
STATUS_ITEM_NOT_FOUND
The item or selector passed to CMD_PROP_REMOVE does not match any item in the property; or the value written by a CMD_PROP_SET names an item of another property that does not exist, as a write of PROP_WIFI_NETWORK names an entry of the known-network table. It is distinct from STATUS_INVALID_ARGUMENT because a host acts on it differently: the value is well-formed and the item it names has merely to be created first.
STATUS_CURSOR_INVALID
The cursor presented in a Node Management continuation is not one the device can honor—it does not parse, it was issued for a different request, or the response it continues is no longer retained. The administrator restarts the read from an initial, cursor-less request.
STATUS_NOT_PERMITTED
The property or command exists, but the binding the request arrived over is not allowed to perform it—in particular, a Node Management write to a property reserved to the tethered host. Distinct from STATUS_PROP_NOT_FOUND and STATUS_INVALID_COMMAND: the operation would be accepted from a binding with the standing to ask.
STATUS_CHANNEL_NOT_FOUND
The request names a channel by its channel identifier and the device holds no channel key that derives it. Distinct from STATUS_ITEM_NOT_FOUND, which concerns an item of the property being written, and from STATUS_INVALID_ARGUMENT: the value is well formed, and the channel has only to be provisioned first (PROP_DEV_CHANNEL_KEYS).
STATUS_NO_DEVICE
An I2C transfer was addressed to a peripheral that did not acknowledge its address: nothing answered on the bus. Distinct from STATUS_ITEM_NOT_FOUND, which is the bus itself not being one the device offers.
STATUS_NACK
The peripheral acknowledged its address but refused a data octet of the transfer. Operations before the refused one have completed.
STATUS_BUS_ERROR
The bus failed under the transaction: arbitration was lost, a bus error or overrun occurred, a line stayed stuck, or the transaction outlasted the device’s deadline. The device may attempt bus recovery.
STATUS_CCA_FAILURE
The packet was not sent due to a CCA failure. This status code is only emitted when sending data to a packet stream with a TID other than zero.
STATUS_DUTY_LIMIT
The packet cannot be sent because it would exceed the currently set duty-cycle limit.

Reset Codes

All status codes which fall into the inclusive range of 112-127 are considered reset codes. These codes are emitted asynchronously after a device reset and provide a way to differentiate different causes of resets. If the first command the host sends to the device after a reset is to fetch PROP_LAST_STATUS, then the reset code MUST be returned.

Note

On a device holding a saved snapshot, the post-reset value of every saved property is its saved value rather than the documented default. A host MUST NOT assume that a reset implies documented factory defaults; it should fetch or explicitly set the properties it depends on. Without a snapshot the documented post-reset values apply unconditionally.

IdName
112STATUS_RESET_POWER_ON
113STATUS_RESET_EXTERNAL
114STATUS_RESET_SOFTWARE
115STATUS_RESET_RESTORED
116STATUS_RESET_CRASH
117STATUS_RESET_ASSERT
118STATUS_RESET_OTHER
119STATUS_RESET_UNKNOWN
120STATUS_RESET_WATCHDOG

Of these defined reset codes, only STATUS_RESET_POWER_ON, STATUS_RESET_EXTERNAL, STATUS_RESET_SOFTWARE, and STATUS_RESET_RESTORED are emitted during normal operation. All other reset codes generally indicate some sort of software bug or hardware failure.

Unexpected or unrequested resets are always an indication of a problem, no matter what the code value is.

A session reset is not one of these. Discarding session state costs the host what it established there and nothing more, so it is announced by CMD_SESSION_RESET and leaves PROP_LAST_STATUS alone. Every reset code above implies a session reset; none of them is implied by one.

STATUS_RESET_POWER_ON
Cold power-on start.
STATUS_RESET_EXTERNAL
External device reset. This is generally caused by RESET pin on the device being asserted.
STATUS_RESET_SOFTWARE
Software-requested orderly reset. This is generally caused by the host sending the device CMD_RST.
STATUS_RESET_RESTORED
Protocol reset into the saved snapshot, emitted when a device completes CMD_RESTORE in its reset form (see CMD_RESTORE). Unlike the other reset codes, this one does not indicate a hardware or firmware restart: the transport link and attach state survive it.
STATUS_RESET_CRASH
Unrecoverable software execution failure, like a segmentation fault or a stack overflow.
STATUS_RESET_ASSERT
Software invariant property not respected.
STATUS_RESET_OTHER
Unspecified cause.
STATUS_RESET_UNKNOWN
Failure while recovering cause of reset.
STATUS_RESET_WATCHDOG
Watchdog timer expired, forcing a reset.

Capabilities

Capabilities are how a device can advertise support for specific behaviors and functionalities. They can be fetched via the PROP_CAPS property.

Each capability is defined by the chapter that specifies the behavior it grants:

CodeNameRequiresDefined in
8CAP_WRITABLE_RAW_STREAM—Frame Transport
16CAP_PHY_DUTY_LIMIT—Radio Control
32CAP_HOST_FILTER—Tethered Host Services
33CAP_HOST_RX_QUEUECAP_HOST_FILTERTethered Host Services
34CAP_HOST_KEYSCAP_HOST_FILTERTethered Host Services
35CAP_HOST_AUTO_ACKCAP_HOST_KEYS, CAP_HOST_RX_QUEUETethered Host Services
36CAP_SAVE—Saved State
37CAP_DEV_IDENTITY—Device Domain
38CAP_DEV_NAME—Device Domain
39CAP_BATTERY—Device Domain
40CAP_REPEATERCAP_DEV_IDENTITYDevice Domain
41CAP_IDENTCAP_DEV_IDENTITYDevice Domain
42CAP_ALERT—Device Domain
43CAP_ADMINCAP_DEV_IDENTITY, CAP_CMD_MULTINode Management
44CAP_TIME—Device Domain
45CAP_GNSSCAP_TIMEDevice Domain
46CAP_ADVERTCAP_DEV_IDENTITYDevice Domain
47CAP_ILLUMINANCE—Device Domain
48CAP_MAC_BACKHAULCAP_DEV_IDENTITYTethered Host Services
49CAP_CMD_MULTI—Framing and Common Semantics
50CAP_BLE—BLE Binding
51CAP_REBOOT—Framing and Common Semantics
52CAP_STATS—Radio Control
53CAP_WIFI_SCAN—Wi-Fi
54CAP_WIFICAP_WIFI_SCANWi-Fi
55CAP_IPV4—IP Connectivity
56CAP_IPV6—IP Connectivity
57CAP_WIFI_APCAP_WIFI_SCANWi-Fi
58CAP_BRIDGE_CLIENTCAP_DEV_IDENTITY; IP family requirement in proseBridge Client
59CAP_DISPLAY_MOTION_WAKE—Device Services
60CAP_I2C—I2C Bus Access
61CAP_TEMPERATURE—Device Domain
62CAP_MESHCORE_FORWARDERCAP_REPEATERDevice Domain
63CAP_LAN_BEARER—IP Multicast
64CAP_DISPLAY—Device Domain
65CAP_KEYBOARD—Device Domain
515CAP_PHY_LORA—Radio Control

A device MUST NOT advertise a capability without also advertising the capabilities it requires. Apart from the multi-property commands, which CAP_CMD_MULTI gates, CMD_REBOOT, CMD_DFU, and CMD_FACTORY_RESET, which CAP_REBOOT gates, CMD_ANNOUNCE, which CAP_ADVERT gates, and the bus-access commands, which CAP_I2C gates, the commands and status codes defined in this chapter are unconditional and need no capability; a device that defines no mutable multi-value properties simply has nothing to apply CMD_PROP_INSERT/CMD_PROP_REMOVE to.

ULCP: Radio Control

Radio control is the subsystem the host uses to configure and observe the physical transceiver: frequency, modulation, transmit power, and the transmit duty-cycle budget. Every ULCP device implements it.

The interface is deliberately not LoRa-specific in shape where that can be avoided. The properties that any radio has—enable, frequency, transmit power, RSSI, MTU—are unconditional; the LoRa modulation parameters are gated behind CAP_PHY_LORA, and duty-cycle accounting behind CAP_PHY_DUTY_LIMIT. Traffic counters are exposed by CAP_STATS.

The RF configuration is device-domain state: it belongs to the radio rather than to whichever host is attached, it is part of a saved snapshot, and it survives a change of host.

Capabilities

CodeNameGrants
16CAP_PHY_DUTY_LIMITDuty-cycle accounting and enforcement: PROP_PHY_DUTY_NOW, PROP_PHY_DUTY_LIMIT, and STATUS_DUTY_LIMIT
52CAP_STATSRadio and forwarding traffic counters: PROP_STAT_*
515CAP_PHY_LORAThe LoRa modulation parameters: PROP_PHY_LORA_BW, PROP_PHY_LORA_SF, PROP_PHY_LORA_CR, PROP_PHY_LORA_SW

Properties

IdMnemonicCommandsDescription
32PROP_PHY_ENABLEDGet, SetPHY enabled
35PROP_PHY_FREQGet, SetFrequency in kHz
37PROP_PHY_TX_POWERGet, SetTX power in dBm
38PROP_PHY_RSSIGetCurrent RSSI
39PROP_PHY_LORA_BWGet, SetLoRa bandwidth
40PROP_PHY_LORA_SFGet, SetLoRa spreading factor
41PROP_PHY_LORA_CRGet, SetLoRa coding rate
42PROP_PHY_MTUGetMax size of a frame
43PROP_PHY_LORA_SWGet, SetLoRa sync word (16-bit style)
44PROP_PHY_T_FRAMEGetMaximum frame airtime in milliseconds
4820PROP_PHY_DUTY_NOWGetCurrent duty usage
4822PROP_PHY_DUTY_LIMITGet, SetDuty-cycle limit
4832PROP_STAT_TX_PACKETSGet, SetPackets transmitted over the air
4833PROP_STAT_TX_CHANNEL_BUSYGet, SetTransmissions deferred because the channel was busy
4834PROP_STAT_RX_PACKETSGet, SetReceived packets with a UMSH first-octet pattern
4835PROP_STAT_RX_BAD_CRCGet, SetReceptions rejected because of a bad CRC
4836PROP_STAT_RX_NON_UMSHGet, SetReceived packets without a UMSH first-octet pattern
4837PROP_STAT_RX_ACCEPTEDGet, SetReceived packets accepted by this device’s node
4838PROP_STAT_FORWARDEDGet, SetPackets this device chose to forward
4839PROP_STAT_FORWARD_DROPPEDGet, SetForwarding candidates rejected by policy
4840PROP_STAT_FORWARD_CANCELLEDGet, SetQueued forwards cancelled after an acknowledgement
4841PROP_STAT_MESHCORE_SEENGet, SetMeshCore packets received
4842PROP_STAT_MESHCORE_FORWARDED_ROUTEDGet, SetMeshCore packets forwarded along their source route
4843PROP_STAT_MESHCORE_FORWARDED_FLOODGet, SetMeshCore packets flooded
4844PROP_STAT_MESHCORE_DECLINEDGet, SetMeshCore packets neither forwarded nor duplicates
4850PROP_STAT_RX_OWN_HINTGet, SetReceived packets whose trace names this device as the last hop
4851PROP_STAT_RADIO_RESETSGet, SetTimes the radio chip was reset after it stopped responding

PROP 32: PROP_PHY_ENABLED

  • Type: Single-Value, Read/Write
  • Asynchronous Updates: No
  • Required:
    • CMD_PROP_GET: REQUIRED
    • CMD_PROP_SET: REQUIRED
  • Scope: NLI
  • Value Type: BOOL
  • Post-Reset Value: 0 (false)

Set to 1 if the PHY is enabled, set to 0 otherwise. Enabling the PHY bypasses nothing: a device with a node of its own brings up whatever that node is configured to do, forwarding included.

PROP 35: PROP_PHY_FREQ

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: UINT32_LE
  • Units: kHz
  • Post-Reset Value: Unspecified

Value is the radio frequency (in kilohertz) of the current channel.

PROP 37: PROP_PHY_TX_POWER

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: INT8
  • Units: dBm
  • Post-Reset Value: Implementation-Specific

Value is the radio transmit power in dBm.

A device MUST clamp a written value to the range its radio can reach rather than rejecting it, and the emitted CMD_PROP_IS carries the clamped value. Nothing else publishes that range, so this is how a host discovers it: a host that needs to know what a device will actually transmit at reads the value it gets back rather than the one it wrote.

PROP 38: PROP_PHY_RSSI

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Value Type: INT8
  • Unit: dBm (RF Power)
  • Post-Reset Value: Unspecified

Value is the current RSSI (Received Signal Strength Indication) from the radio. This value can be used in energy scans and for determining the ambient noise floor for the operating environment.

Zero dBm represents one milliwatt of power.

Sampling ambient RSSI requires the radio to be actively receiving. If PROP_PHY_ENABLED is false, getting this property fails with STATUS_INVALID_STATE. A get may also fail with STATUS_FAILURE if the radio cannot service the read (for example, mid-reconfiguration).

PROP 39: PROP_PHY_LORA_BW

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT32_LE
  • Units: Hz
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa bandwidth.

PROP 40: PROP_PHY_LORA_SF

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT8
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa spreading factor.

PROP 41: PROP_PHY_LORA_CR

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT8
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa coding rate.

PROP 42: PROP_PHY_MTU

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: UINT16_LE
  • Units: octets
  • Post-Reset Value: Implementation-Specific

Maximum size of the DATA field that may be supplied to STR_PHY_RAW.

PROP 43: PROP_PHY_LORA_SW

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT16
  • Post-Reset Value: Implementation-Specific, but 0x1424 is a good suggestion.

Value is the 16-bit (SX126x-style) LoRa sync-word.

PROP 44: PROP_PHY_T_FRAME

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: REQUIRED for new implementations; older devices may return STATUS_PROP_NOT_FOUND
  • Scope: NLI
  • Value Type: UINT32_LE
  • Units: milliseconds
  • Post-Reset Value: Derived from the post-reset PHY configuration

An upper bound on the on-air duration of a frame with PROP_PHY_MTU data octets at the current PHY settings, rounded up to a whole millisecond. Implementations MAY round conservatively upward. The positive, nonzero value includes the transmit preamble, PHY header, coding overhead, and PHY CRC. It excludes channel-access waiting, retry delays, duty-cycle deferral, queueing, and host transport latency. It remains readable while the PHY is disabled and is recomputed from the current configuration.

This is the PHY-independent T_frame used by channel-access timing. Hosts can use it without interpreting modulation-specific properties. A host SHOULD refresh it after PHY configuration changes and before choosing a transmit response deadline; max(3000 ms, 2 × T_frame) is a suggested budget. An explicit failure response, including STATUS_CCA_FAILURE, completes the request immediately rather than waiting for this deadline.

Older LoRa devices can be supported by deriving an estimate from their live PHY parameters, with an implementation-specific assumption about the preamble when it is not exposed. Hosts MUST NOT substitute their desired or default configuration for the device’s current configuration.

PROP 4820: PROP_PHY_DUTY_NOW

  • Type: Single-Value, Read-Only
  • Value Type: u16
  • Units: Percent, 0-65535 -> 0-100%
  • Post-Reset Value: 0%
  • Required Capability: CAP_PHY_DUTY_LIMIT

The radio transmit duty cycle over the past hour, updated in 4-minute intervals.

Under the hood, this is represented as 15 16-bit bins, one for each 4-minute interval. An increment of 1 represents 5ms. For each 5ms of transmission time, the current bin is incremented by 1. So a 20ms transmission would increment the current bin by 4, but a 22ms transmission would increment the bin by 5. At the transition between intervals, the new current bin is reset to zero.

To calculate the current duty cycle, all of the bins are added together, multiplied by 65535, and then divided by 720000.

PROP 4822: PROP_PHY_DUTY_LIMIT

  • Type: Single-Value, Read-Write
  • Value Type: u16
  • Units: Percent, 0-65535 -> 0-100%
  • Post-Reset Value: Settings-dependent
  • Required Capability: CAP_PHY_DUTY_LIMIT

The value for PROP_PHY_DUTY_NOW above which sending additional packets will be prevented. Packets that are prevented from being sent will be dropped with STATUS_DUTY_LIMIT.

Set to 0xFFFF to disable duty-cycle limiting. Note that PROP_PHY_DUTY_NOW will continue to be updated even if duty-cycle limiting is disabled.

Values for common duty cycles:

ValuePercentage
1310720%
655310%
6551%
650.1%

Statistics

A device advertising CAP_STATS exposes cumulative UINT32_LE traffic counters. Values count since boot or the most recent reset of that counter and wrap modulo 2^32. Writing four zero octets resets a counter. A device MUST reject every other value or length with STATUS_INVALID_ARGUMENT.

Statistics are live hardware history rather than configuration. They are not part of a saved snapshot, and CMD_RST MUST NOT clear them. A host can reset several counters with CMD_PROP_MULTI_SET, but the individual resets do not occur simultaneously: traffic arriving during the write sweep can fall on different sides of different counter resets.

PROP_PHY_DUTY_NOW is commonly displayed alongside these counters, but it is a rolling regulatory measurement and is not resettable.

PROP 4832: PROP_STAT_TX_PACKETS

Packets whose transmission completed successfully over the physical radio. Requires CAP_STATS.

PROP 4833: PROP_STAT_TX_CHANNEL_BUSY

Transmission attempts deferred because channel assessment reported the channel busy. Such an attempt does not also increment PROP_STAT_TX_PACKETS. Requires CAP_STATS.

PROP 4834: PROP_STAT_RX_PACKETS

Off-air receptions whose first octet carries the UMSH version and valid reserved bits. This classification does not require the rest of the packet to parse. Requires CAP_STATS.

PROP 4835: PROP_STAT_RX_BAD_CRC

Receptions the physical radio rejected because their payload CRC was invalid. Requires CAP_STATS.

PROP 4836: PROP_STAT_RX_NON_UMSH

Off-air receptions whose first octet does not carry the UMSH version and valid reserved bits. Requires CAP_STATS.

PROP 4837: PROP_STAT_RX_ACCEPTED

Receptions on which this device’s own node acted. Frames addressed to an attached host’s identity are not included. Requires CAP_STATS and CAP_REPEATER.

PROP 4838: PROP_STAT_FORWARDED

Receptions this device’s own node chose to repeat. The counter records the forwarding decision; successful physical transmissions are counted separately by PROP_STAT_TX_PACKETS. Requires CAP_STATS and CAP_REPEATER.

PROP 4839: PROP_STAT_FORWARD_DROPPED

Packets that were eligible for forwarding but were rejected by this repeater’s configured minimum RSSI, minimum SNR, or region policy. Exhausted flood budgets and packet-imposed signal thresholds are normal packet behavior, not administrator policy, and are not included. Duplicates, self-addressed packets, and packets received while forwarding is disabled are also excluded. Requires CAP_STATS and CAP_REPEATER.

PROP 4840: PROP_STAT_FORWARD_CANCELLED

Queued forwards cancelled because the destination’s acknowledgement was overheard before transmission. Requires CAP_STATS and CAP_REPEATER.

PROP 4841: PROP_STAT_MESHCORE_SEEN

MeshCore packets received while MeshCore forwarding is running. Every one is also counted in exactly one of PROP_STAT_MESHCORE_FORWARDED_ROUTED, PROP_STAT_MESHCORE_FORWARDED_FLOOD, or PROP_STAT_MESHCORE_DECLINED, or is a duplicate. Copies of this device’s own transmissions are not received packets. Requires CAP_STATS and CAP_MESHCORE_FORWARDER.

PROP 4842: PROP_STAT_MESHCORE_FORWARDED_ROUTED

MeshCore packets this device chose to forward along their source route. The counter records the forwarding decision; successful physical transmissions are counted by PROP_STAT_TX_PACKETS. Requires CAP_STATS and CAP_MESHCORE_FORWARDER.

PROP 4843: PROP_STAT_MESHCORE_FORWARDED_FLOOD

MeshCore packets this device chose to flood, counted at the decision as above. Requires CAP_STATS and CAP_MESHCORE_FORWARDER.

PROP 4844: PROP_STAT_MESHCORE_DECLINED

MeshCore packets this device neither forwarded nor recognized as duplicates: declined by MeshCore’s own forwarding rules or by the region policy, below the configured signal thresholds, malformed, or dropped for want of queue space. Requires CAP_STATS and CAP_MESHCORE_FORWARDER.

PROP 4850: PROP_STAT_RX_OWN_HINT

Packets received over the air whose trace begins with this device’s own router hint. A radio cannot receive its own transmission, so each is a copy of one of this device’s repeats retransmitted without a hop being added, or a packet repeated by another node sharing this device’s router hint. Packets received over a point-to-point link or a shared IP bearer are not counted. None of these names a neighbor, and a device does not list itself in a Peer Repeaters Response. Requires CAP_STATS and CAP_REPEATER.

PROP 4851: PROP_STAT_RADIO_RESETS

Times the device reset its radio chip because the chip stopped doing what it was told: a transmission that did not complete within its time limit, or any other radio operation that did not finish. A transmission abandoned this way increments neither PROP_STAT_TX_PACKETS nor PROP_STAT_TX_CHANNEL_BUSY. Requires CAP_STATS.

ULCP: Frame Transport

Frame transport is the data plane: the host transmits and receives raw, complete UMSH frames through the device. Frames cross the link untouched—the ULCP link carries UMSH frames, not re-encoded UMSH semantics—so the host runs the entire UMSH MAC and the device moves frames on its behalf.

One practical consequence is that a host can address the device’s own device identity with ordinary UMSH frames over this stream, rather than needing a separate bespoke message path for such traffic.

There is no outbound queueing. A transmit either happens or fails while the host is attached to observe the result. Inbound frames may be queued while no host is attached, which is a tethered host service rather than a property of the stream.

Capabilities

CodeNameGrants
8CAP_WRITABLE_RAW_STREAMSTR_PHY_RAW accepts host-originated transmission through CMD_STR_SEND

Reception needs no capability, and CMD_STR_SEND support is required of every device (see Minimum Requirements). The capability is an affirmative advertisement that transmission is available, not a gate on the command.

Commands

IdMnemonicDirDescription
9CMD_STR_SENDHost->DeviceSend data to a stream
10CMD_STR_RECVDevice->HostReceive data from a stream

CMD 9: (Host -> Device) CMD_STR_SEND

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | STREAM_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   DATA_LEN (Little endian)    | DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_STR_SEND

Command for sending data (such as a packet) to a stream.

The format of the metadata is defined by the stream, and may be absent. Since the framing layer provides the total frame length, DATA_LEN is sufficient to determine the length of both the data and any trailing metadata.

If a non-zero TID is used, the command completes only once the frame has either been transmitted on air or definitively failed. Success is reported by emitting CMD_PROP_IS for PROP_LAST_STATUS with STATUS_OK and a matching TID.

The device only attempts one confirmed transmit at a time. If a CMD_STR_SEND with a non-zero TID arrives while another confirmed transmit is in progress, the new command fails with STATUS_BUSY.

The device will never wait for duty-cycle allowance. If transmission would exceed the currently configured duty-cycle limit and the NODUTY flag is not set, the command fails immediately with STATUS_DUTY_LIMIT.

Commands sent with TID zero are fire-and-forget and do not receive a correlated completion response.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 10: (Device -> Host) CMD_STR_RECV

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES |0 0 0|      CMD      | STREAM_KEY (PUI, 1-3 bytes)...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   DATA_LEN (Little endian)    | DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_STR_RECV

Notification of incoming data received on the given stream. Because this command is only ever sent asynchronously, the TID is always zero.

The format of the metadata is defined by the stream, and may be absent. Since the framing layer provides the total frame length, DATA_LEN is sufficient to determine the length of both the data and any trailing metadata.

Streams

IdMnemonicCommandsDescription
113STR_PHY_RAWSend, RecvRaw radio frame stream

STREAM 113: STR_PHY_RAW

  • Type: Packet-Stream, Input/Output
  • Required: REQUIRED
  • Supported Commands: Send, Recv
  • Scope: NLI
  • Value Type: Structure
  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   PACKET_LEN (Little endian)  | PACKET_DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   PACKET_METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

This stream provides the capability of sending and receiving raw packets to and from the radio.

The packet metadata is optional, but if present will be after the packet data and will have the following format:

Metadata for Send

The Send metadata is the following fields in order:

  • TX_POWER (i8): Transmit power override (0x7F indicates to use the radio default, 0x7E indicates to transmit at maximum power). A power the radio cannot reach is clamped to its range, as for PROP_PHY_TX_POWER.
  • TX_FLAGS (u8): Transmit flags
    • TX_FLAG_NOCCA Bit 0: If set, do not use CCA (or the equivalent LoRa mechanism)
    • TX_FLAG_NODUTY Bit 1: If set, send the packet even if it would push us over the duty-cycle limit
    • All other bits: RESERVED

Metadata for Recv

The Recv metadata is the following fields in order:

  • RX_RSSI (u8): This is the negative RSSI that this packet was received with. So if the RSSI was -91, the value of this field would be 91.
    • If 0xFF, no RSSI is supported.
  • RX_LQI (u8): This is the link-quality indicator, which is a metric of link quality between 1 and 255 with 1 being the worst possible quality that still decodes and 255 is perfect reception.
    • If 0x00, LQI is not supported.
  • RX_SNR (i16): Signal-to-noise ratio in centibels, or 1/10 of a decibel.
    • If 0x8000 (i16::MIN), SNR is not supported. This sentinel is chosen because it is -3276.8 dB, a value no real link can report, so it never collides with a genuine measurement (unlike 0xFFFF, which is -0.1 dB).

Extended Recv Metadata

The Recv metadata may carry two further trailing fields:

  • RX_FLAGS (u8): Delivery flags
    • RX_FLAG_BUFFERED Bit 0: The frame was held in the inbound queue and is being delivered by CMD_QUEUE_DRAIN.
    • RX_FLAG_ACKED Bit 1: The device already transmitted a MAC ack for this frame on the host’s behalf. The host MUST NOT send another ack for it.
    • RX_FLAG_SELF_TX Bit 2: The device transmitted this frame itself and is delivering a copy of it. RX_RSSI and RX_SNR MUST carry their unsupported sentinels: a transmitter measures nothing.
    • All other bits: RESERVED, transmitted as zero
  • RX_AGE (u32, little-endian): Seconds elapsed between reception of the frame and its delivery to the host. Zero for live delivery.

As with the existing metadata fields, the metadata may be truncated at any field boundary; absent fields are treated as zero. A live delivery with nothing to flag MAY therefore omit these fields entirely, which keeps the encoding byte-compatible with a device that does not queue at all.

ULCP: Device Domain

The device domain is everything that belongs to the radio itself: its own node identity, the settings that make it behave the way its operator commissioned it, and the telemetry it can report about its own hardware. None of it is keyed by the attached host, and none of it is disturbed when one host replaces another (see State Classes).

Commissioning a repeater is exactly this: writing device-domain state and saving it. It is also the reason a host that is merely administering a device writes nothing in the host domain—see Two Kinds of Attach.

The Device Identity

The device hosts a node belonging to the device itself, used for in-band management, diagnostics, repeater forwarding (see PROP_MAC_REPEATER_ENABLED), and (in future revisions) periodic advertisement behavior. Its Ed25519 private key is held by the device and is never readable through this protocol.

On a device with a messaging interface of its own (see The Device’s Own Interface), the device identity is also the messaging identity of whoever carries the device: its conversations are held under it, and its peers address it. Such a device advertises the TXT capability bit (see Capability Bitmap), derives its role as Chat (see PROP_IDENT_ROLE), and advertises itself as mobile by default (see PROP_IDENT_MOBILE).

A device identity always exists. A device advertising CAP_DEV_IDENTITY that finds no stored keypair at power-on MUST generate one from a cryptographic random source and persist it before processing any host command; PROP_DEV_KEY therefore never reports an empty value on a running device. Provisioning an identity is not a commissioning step: a factory-fresh radio is already a node, and PROP_DEV_PRIVATE_KEY exists to install a particular identity—restoring a known repeater onto replacement hardware—not to bring one into being.

The corollary is that a radio holds a throwaway identity from first power-on until a specific one is installed. This is safe because it never reaches the air: PROP_PHY_ENABLED is false post-reset, and a radio with nothing saved boots with the PHY disabled. It is not safe automatically on the restore path—see CMD_RESTORE.

Because the device holds this identity’s private key, it performs its own key agreement and needs only peer public keys (see PROP_DEV_PEERS). That is the opposite of the host identity, for which the device holds no private key and every pairwise key must be provisioned explicitly (see Tethered Host Services).

Lists Held by the Device’s Store

A device with a messaging interface of its own keeps a store for its conversations, and that store MAY also keep the device identity’s lists, PROP_DEV_PEERS and PROP_DEV_CHANNEL_KEYS: whom its carrier talks with and on which channels belong with the conversations, not with the radio’s configuration.

Lists the store keeps are persisted as they change, as the device identity is, and are outside the saved snapshot (see Saved State):

  • CMD_SAVE does not record them, and CMD_RESTORE leaves them as they are.
  • CMD_RST restores them as last stored.
  • CMD_CLEAR erases them with the rest of the persisted provisioning, and leaves the conversations.

With none stored, PROP_DEV_PEERS is empty and PROP_DEV_CHANNEL_KEYS holds the channels the messaging interface starts out on: the default channels, public and EMERGENCY.

Capabilities

CodeNameRequiresGrants
37CAP_DEV_IDENTITY—The device identity: PROP_DEV_KEY, PROP_DEV_PRIVATE_KEY, PROP_DEV_CHANNEL_KEYS, PROP_DEV_PEERS
38CAP_DEV_NAME—PROP_DEV_NAME
39CAP_BATTERY—Battery-powered operation and PROP_BATTERY
40CAP_REPEATERCAP_DEV_IDENTITYAutonomous repeater forwarding by the device identity: PROP_MAC_REPEATER_ENABLED, PROP_MAC_REPEATER_REGIONS, PROP_MAC_REPEATER_DEFAULT_REGION, PROP_MAC_REPEATER_MIN_RSSI, PROP_MAC_REPEATER_MIN_SNR
41CAP_IDENTCAP_DEV_IDENTITYPROP_IDENT, PROP_IDENT_ROLE, PROP_IDENT_MOBILE, PROP_IDENT_LOCATION, PROP_IDENT_ALTITUDE, PROP_IDENT_CONTACT—serving and configuring the device identity’s advertised node identity
42CAP_ALERT—Some means of making the device physically conspicuous on demand, and PROP_ALERT
44CAP_TIME—A wall clock: PROP_TIME, PROP_TZ_RULE, PROP_TIME_24H
45CAP_GNSSCAP_TIMEA GNSS receiver: PROP_GNSS_ENABLED, PROP_GNSS_LOCATION, PROP_GNSS_ALTITUDE, PROP_GNSS_FIX, PROP_GNSS_PRECISION, PROP_GNSS_SATELLITES, PROP_GNSS_IDENT_UPDATE, PROP_GNSS_IDENT_PRECISION, PROP_GNSS_TIME_TRUST
46CAP_ADVERTCAP_DEV_IDENTITYAnnouncing itself: on a schedule of its own (PROP_ADVERT_INTERVAL, PROP_BEACON_INTERVAL, PROP_STARTUP_BEACON) and on demand (CMD_ANNOUNCE)
47CAP_ILLUMINANCE—An ambient light sensor and PROP_ILLUMINANCE
59CAP_DISPLAY_MOTION_WAKE—Orientation-qualified display wake and PROP_DISPLAY_MOTION_WAKE_ENABLED
61CAP_TEMPERATURE—PROP_TEMPERATURES and PROP_TEMPERATURE_NAMES; the inventory may initially be empty
62CAP_MESHCORE_FORWARDERCAP_REPEATERMeshCore forwarding: PROP_MESHCORE_FORWARDING_ENABLED, PROP_MESHCORE_REGION_POLICY, PROP_MESHCORE_REGIONS; with CAP_STATS, the MeshCore counters
64CAP_DISPLAY—A screen of the device’s own: PROP_DISPLAY_BRIGHTNESS, PROP_DISPLAY_TIMEOUT
65CAP_KEYBOARD—A keyboard of the device’s own: PROP_KEYBOARD_LIGHT, PROP_KEYBOARD_AIDS, PROP_SETUP_COMPLETE
66CAP_SOUND—A sounder the device plays optional sounds on: PROP_SOUND_ENABLED, PROP_SOUND_VOLUME

CAP_ADVERT requires CAP_DEV_IDENTITY because what an advertisement carries is the device identity, and a beacon’s source address names it.

CAP_TIME states that the device keeps a wall clock and nothing else. It says nothing about where the time comes from, how accurate it is, or how much of a power cycle it survives—a device that has one and does not currently know what time it is is a normal state, reported by the empty PROP_TIME.

CAP_GNSS requires CAP_TIME because a receiver is, among other things, a clock: a device advertising one without the other would be claiming a time source for a clock it does not have.

Commands

CMD 19: (Host -> Device) CMD_ANNOUNCE

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+
|1 0| RES | TID |  CMD_ANNOUNCE |   OPTIONS   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+

Figure: Structure of CMD_ANNOUNCE

Announce the device now, outside either schedule. What the device sends is an advertisement or a beacon—the same two announcements PROP_ADVERT_INTERVAL and PROP_BEACON_INTERVAL emit unasked—as a broadcast or as multicast on one of the device’s own channels.

OPTIONS is a sequence of entries in the CMD_PROP_MULTI_SET entry form: LENGTH (PUI) | OPTION (PUI) | VALUE. An empty payload is a well-formed request carrying every default.

OptionNameValueDefault
1ANNOUNCE_KINDUINT8: 0 advertisement, 1 beacon0
2ANNOUNCE_FLOOD_HOPSUINT8, 0–150
3ANNOUNCE_FULL_SOURCEBOOL1 for an advertisement, 0 for a beacon
4ANNOUNCE_CHANNEL16 octets: the full channel identifierabsent: broadcast
ANNOUNCE_KIND
An advertisement carries the device’s node identity in a signed frame; a beacon carries no payload at all.
ANNOUNCE_FLOOD_HOPS
The flood budget the frame goes out with (FHOPS_REM, see Packet Structure). Zero sends no flood-hop field, reaching only the nodes that hear the device directly. A frame carrying a budget also carries the Trace option, so what arrives at a distant node is a usable path back; a beacon carries it at any budget, a path being what a beacon publishes.
ANNOUNCE_FULL_SOURCE
Whether SRC is the 32-byte public key rather than the 3-byte hint. An advertisement’s frame signature is checkable only against the device’s key, which a listener that has not met the device holds only from a full SRC, hence its default; a beacon has nothing to check and defaults to the hint. A host may override either way.
ANNOUNCE_CHANNEL
Send as multicast on the device channel whose full channel identifier this is—the form that names a channel unambiguously, since two keys can share a 2-byte channel_id. It is what PROP_DEV_CHANNEL_KEYS reports for each key the device holds, so a host names a channel with exactly what it read back. Absent, the announcement is a broadcast. A multicast announcement uses the channel’s encrypted mode.

An option the device does not recognize, a repeated option, or a value of the wrong length or out of range is STATUS_INVALID_ARGUMENT. A device MUST NOT silently ignore an unknown option: the host asked for something specific, and a broadcast sent in place of an ignored multicast request would be the wrong frame on the air. A malformed entry list is STATUS_PARSE_ERROR.

The device answers with CMD_PROP_IS of PROP_LAST_STATUS carrying the command’s TID:

  • STATUS_OK once the announcement is queued for transmission. This command reports queuing, not airing: channel access and the duty limit decide later whether the frame reaches the air, exactly as for a scheduled announcement.
  • STATUS_BUSY when an announcement is already pending and this one could not be queued behind it.
  • STATUS_CHANNEL_NOT_FOUND when ANNOUNCE_CHANNEL names a channel the device holds no key for.
  • STATUS_UNIMPLEMENTED on a device without CAP_ADVERT, or one with no node behind the session to announce.

This command is available on devices advertising CAP_ADVERT. It carries no tethered-host state, so an administrator may issue it over the Node Management binding.

Properties

The device domain occupies property identifiers 64–95. Identifiers 70–95 are the device-behavior range: 70–78 are the repeater policy and advertised node identity settings, 79 is the locate alert, 80–87 are what the device announces about itself—80–82 the advertisement schedule, 83–84 the advertised position, 85–87 reserved—88–93 are positioning (88 the receiver switch, 89–93 the fix telemetry), and 94–95 are environmental sensing: 94 illuminance, 95 temperatures.

The advertised position at 83–84 and the fix telemetry at 89–90 are deliberately separate properties. The fix is what the receiver currently sees; the advertised position is what the identity claims, which a device without a receiver still has.

A single-octet identifier is the scarce resource, so the positioning range holds the properties a host reads and the device announces continually. The positioning configuration—which is written during commissioning and rarely again—lives at 4868–4870 in the extended device range, alongside the wall clock at 4866 and 4888–4889.

IdMnemonicCommandsDescription
64PROP_DEV_KEYGetDevice identity public key
65PROP_DEV_PRIVATE_KEYSetDevice identity private key (write-only)
66PROP_DEV_CHANNEL_KEYSGet, Set, Insert, RemoveDevice identity channel keys
67PROP_DEV_PEERSGet, Set, Insert, RemoveDevice identity peer list
68PROP_DEV_NAMEGet, SetHuman-readable device name
69PROP_BATTERYGet, IsBattery status snapshot
4944PROP_BATTERY_CURRENTGet, IsBattery diagnostics
4945PROP_BATTERY_REMAINING_CAPACITYGet, IsBattery diagnostics
4946PROP_BATTERY_FULL_CAPACITYGet, IsBattery diagnostics
4947PROP_BATTERY_DESIGN_CAPACITYGet, IsBattery diagnostics
4948PROP_BATTERY_EXT_POWER_PRESENTGet, IsBattery diagnostics
4949PROP_BATTERY_PRESENTGet, IsBattery diagnostics
4950PROP_BATTERY_GAUGE_FULLGet, IsBattery diagnostics
4951PROP_BATTERY_GAUGE_INITIALIZEDGet, IsBattery diagnostics
4952PROP_BATTERY_GAUGE_SMOOTHINGGet, IsBattery diagnostics
4953PROP_BATTERY_CHARGE_VOLTAGE_REQUESTGet, IsBattery diagnostics
4954PROP_BATTERY_GAUGE_FORMATGet, IsBattery diagnostics
4955PROP_BATTERY_GAUGE_STATUSGet, IsBattery diagnostics
4956PROP_BATTERY_GAUGE_OPERATION_STATUSGet, IsBattery diagnostics
4957PROP_BATTERY_GAUGE_CONFIGGet, IsOn-demand gauge configuration
4958PROP_BATTERY_GAUGE_TELEMETRYGet, IsLive gauge counters and estimates
70PROP_MAC_REPEATER_ENABLEDGet, SetAutonomous repeater forwarding enable
71PROP_IDENTGetSigned node identity of the device identity
72PROP_IDENT_ROLEGet, SetAdvertised node role, or empty to derive it
73PROP_IDENT_MOBILEGet, SetAdvertise the mobile capability bit
74PROP_MAC_REPEATER_REGIONSGet, Set, Insert, RemoveRegion strings the device forwards for
75PROP_MAC_REPEATER_DEFAULT_REGIONGet, SetRegion code inserted into untagged flood packets
76PROP_MAC_REPEATER_MIN_RSSIGet, SetMinimum received RSSI for flood forwarding
77PROP_MAC_REPEATER_MIN_SNRGet, SetMinimum received SNR for flood forwarding
78PROP_DEV_DISCOVERABLEGet, SetWhether the device identity answers Identity Requests
79PROP_ALERTGet, Set, IsLocate alert state
80PROP_ADVERT_INTERVALGet, SetSeconds between unsolicited advertisements
81PROP_BEACON_INTERVALGet, SetSeconds between unsolicited beacons
82PROP_STARTUP_BEACONGet, SetWhether a beacon goes out at bring-up
83PROP_IDENT_LOCATIONGet, SetPosition the advertised node identity carries
84PROP_IDENT_ALTITUDEGet, SetAltitude the advertised node identity carries
85PROP_IDENT_CONTACTGetSigned node identity for keeping as a contact
88PROP_GNSS_ENABLEDGet, SetWhether the GNSS receiver is powered
89PROP_GNSS_LOCATIONGet, IsPosition of the last fix
90PROP_GNSS_ALTITUDEGetAltitude of the last fix
91PROP_GNSS_FIXGet, IsFix quality
92PROP_GNSS_PRECISIONGetEstimated horizontal accuracy of the last fix
93PROP_GNSS_SATELLITESGetSatellites used, and optionally in view
94PROP_ILLUMINANCEGetAmbient illuminance in millilux
95PROP_TEMPERATURESGetOrdered temperature readings in tenths of kelvin
4876PROP_TEMPERATURE_NAMESGetOrdered temperature sensor names
4875PROP_DISPLAY_MOTION_WAKE_ENABLEDGet, Set, IsWhether movement wakes the display
4877PROP_DISPLAY_BRIGHTNESSGet, Set, IsScreen brightness
4890PROP_DISPLAY_TIMEOUTGet, Set, IsHow long the screen stays lit without input
4878PROP_KEYBOARD_LIGHTGet, Set, IsWhether the keyboard is lit while the screen is
4879PROP_KEYBOARD_AIDSGet, Set, IsTyping aids in effect
4891PROP_SOUND_ENABLEDGet, Set, IsWhether optional sounds are played
4892PROP_SOUND_VOLUMEGet, Set, IsHow loud optional sounds are played
4893PROP_SETUP_COMPLETEGet, Set, IsWhether the device has been set up through its own interface
4894PROP_TEST_MESSAGE_ALERTSGet, Set, IsWhether test messages on EMERGENCY call for attention
4866PROP_TIMEGet, Set, IsWall clock, or empty when unknown
4888PROP_TZ_RULEGet, Set, IsLocal time zone, as a POSIX TZ rule
4889PROP_TIME_24HGet, Set, IsWhether shown clocks use 24-hour time
4868PROP_GNSS_IDENT_UPDATEGet, SetWhether fixes update the advertised node identity
4869PROP_GNSS_IDENT_PRECISIONGet, SetPrecision the advertised location is clamped to
4870PROP_GNSS_TIME_TRUSTGet, Set, IsWhether receiver-derived time may set the clock
4976PROP_MESHCORE_FORWARDING_ENABLEDGet, SetMeshCore forwarding enable
4977PROP_MESHCORE_REGION_POLICYGet, SetHow MeshCore transport codes gate flood forwarding
4978PROP_MESHCORE_REGIONSGet, Set, Insert, RemoveMeshCore region names the device forwards scoped floods for

The RF configuration is also device-domain state, but is specified in Radio Control; so is the transport configuration in ULCP over BLE.

PROP 64: PROP_DEV_KEY

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Persisted

The Ed25519 public key of the device identity (see The Device Identity). The public key is also emitted as the success response when the private key is installed or generated (see PROP_DEV_PRIVATE_KEY).

An empty value means the device has no device identity. A conforming device does not report one in normal operation—an identity is generated at first boot if none is stored—so hosts SHOULD treat an empty value as a fault to surface rather than as an invitation to provision one.

Frames addressed to the device identity are processed by the device itself. They are additionally delivered or queued to the host only if they independently match the host’s receive filtering (see Receive Filtering).

PROP 65: PROP_DEV_PRIVATE_KEY

  • Type: Single-Value, Write-Only
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Persisted

Installs or generates the device identity private key. An identity always exists already (see The Device Identity), so both forms replace one:

  • Setting a 32-octet value installs it as the device identity’s Ed25519 private key. This is the recovery path—moving a known repeater’s identity onto replacement hardware—not a commissioning step.
  • Setting an empty value commands the device to generate a fresh private key entirely on-device from its cryptographically secure random number generator. On-device generation is RECOMMENDED over installation, since a generated key never exists anywhere but the radio.

In both cases, success is reported by emitting CMD_PROP_IS for PROP_DEV_KEY—carrying the resulting public key—with the command’s TID. The private key itself is never emitted. Success MUST NOT be reported before the new identity is in effect and durably stored. Replacing an existing device identity is permitted; implementations SHOULD treat the device identity’s peer list and channel keys as still valid, since they are not derived from the identity key.

This property is write-only: CMD_PROP_GET MUST fail with STATUS_UNIMPLEMENTED and MUST NOT disclose the value or whether an identity is configured (use PROP_DEV_KEY for that).

The device identity is not part of the saved snapshot (see Saved State): it is durably persisted as soon as it is installed or generated, and it is changed only by another set of this property or by CMD_CLEAR. CMD_RESTORE never reverts it—though it does read the identity a snapshot was taken under, and refuses to enable the PHY when it does not match (see CMD_RESTORE).

Replacing the identity takes effect for the property surface immediately and for anything the device built around the old key at the next boot. The old key stops being one the device claims at once, so a device running a device node MUST stop originating traffic under it rather than continue until the reboot.

Installing a private key is subject to the same transport security requirements as all key provisioning (see Provisioning Security).

PROP 66: PROP_DEV_CHANNEL_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Item Form: 32 octets (the channel key)
  • Reported Form: 16 octets (the derived channel identifier)
  • Remove Selector: the 32-octet channel key
  • Post-Reset Value: Empty, or restored from saved state or from the device’s store (see Lists Held by the Device’s Store)

The set of channel keys belonging to the device identity—channels the radio’s own node participates in (for example, a site-infrastructure management channel). These are independent of the host domain: they survive host replacement and are distinct from PROP_HOST_CHANNEL_KEYS.

For each key the device derives the channel identifier and the channel’s K_enc/K_mic (see Multicast Packet Keys). Each entry is reported as its full 16-octet channel identifier, which is what names one of these channels to a management interface—two keys can derive the same 2-byte channel_id, and CMD_ANNOUNCE must not be left to guess which was meant. The key itself is never read back.

Device channel keys do not create implicit host receive filters: frames on these channels are consumed by the device node and reach the host only through the host’s own filtering.

PROP 67: PROP_DEV_PEERS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Item Form: 32 octets (the peer’s Ed25519 public key)
  • Remove Selector: the 32-octet public key
  • Post-Reset Value: Empty, or restored from saved state or from the device’s store (see Lists Held by the Device’s Store)

The device identity’s peer list: the set of peer public keys the device node recognizes and may communicate with securely. Because the device holds the device identity’s private key, it performs its own key agreement (Unicast Key Agreement) for these peers—no symmetric keys are provisioned, and the entries contain no secret material.

A device MAY hold more peers than one value can carry. CMD_PROP_GET on a list too long to return is answered with STATUS_NOMEM, never with some of its items; CMD_PROP_INSERT and CMD_PROP_REMOVE still apply.

How the device node uses this list (management access control, secure diagnostics, and so on) is application behavior outside the scope of this protocol.

PROP 68: PROP_DEV_NAME

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_DEV_NAME
  • Value Type: 1–64 octets of UTF-8, without U+0000
  • Post-Reset Value: Implementation-defined default, or restored from saved state

The operator-assigned, human-readable name of the physical device. It is independent of the device and host cryptographic identities and MUST NOT be derived from a bonded host or other host-domain state.

Setting the property changes the live name immediately. Like other ordinary device-domain configuration, it is included in a CMD_SAVE snapshot but is not independently persisted merely by being set. Applications and transports that present the device to a person SHOULD use this value when practical. They MAY shorten it to fit a constrained presentation, but MUST NOT split a UTF-8 code point when doing so.

The name is intentionally public metadata. Operators should assume that any value used in discovery advertisements can be observed by nearby devices.

PROP 69: PROP_BATTERY

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_BATTERY
  • Value Type: Battery status snapshot (see below), or empty
  • Post-Reset Value: Current measurement, or empty if reporting is unsupported

A device advertising CAP_BATTERY has a battery capable of powering its operation and recognizes this property. The capability does not require the hardware to support reporting any measurement: an implementation that cannot report battery status at all answers CMD_PROP_GET successfully with an empty value.

A non-empty value is a snapshot of the battery measurements the platform supports, taken as one measurement event:

OctetsField
1Field flags
0 or 2Battery voltage, UINT16_LE, millivolts
0 or 1Battery level, UINT8, percent (0–100)
0+Charge state, PUI

Bits 0 (voltage), 1 (level), and 2 (charge state) of the field flags octet indicate which fields are present; present fields follow in the order above. Bits 3–7 are reserved and MUST be zero; a host MUST treat a value with a reserved bit set, or whose length does not match its field flags, as malformed.

Which fields a platform can report is fixed for a given hardware and firmware configuration; an individual snapshot carries those it can currently substantiate. A field is absent either because the implementation never reports that measurement, or because the value is not derivable in the device’s present state—a level estimated from resting terminal voltage is not obtainable while the pack is charging, and a charger that reports no completion signal offers no moment at which to recalibrate one. An implementation MUST NOT report a value it knows to be unreliable in place of omitting the field.

Absence MUST NOT be used to indicate a depleted or disconnected battery, and it is not how a failed measurement is reported: an implementation whose attempt to take a reading fails answers CMD_PROP_GET with STATUS_FAILURE.

A host MUST treat an absent field as unknown at that instant, and MUST NOT carry a value forward from an earlier snapshot in its place.

The value returned by CMD_PROP_GET reflects a measurement performed when the request is serviced, not a previously cached reading; concurrent requests MAY share one measurement. How each field is produced is platform-defined—in particular, the level estimate is not necessarily derived from the voltage measurement, and a platform with a fuel gauge may report a level without reporting a voltage at all.

The fields:

Battery voltage
The measured voltage at the battery terminals, in millivolts. This is the battery voltage, not an external-power input or regulated system voltage; it may therefore reflect the normal voltage elevation that occurs while the battery is charging.
Battery level
The implementation’s estimate of the battery’s state of charge, as an integer percentage from 0 through 100 inclusive. A host MUST NOT derive this value from the voltage field or assume that successive estimates change monotonically.
Charge state
The current battery charge state:
ValueName
0BATTERY_CHARGE_STATE_DISCHARGING
1BATTERY_CHARGE_STATE_CHARGING
2BATTERY_CHARGE_STATE_CHARGED
3BATTERY_CHARGE_STATE_NOT_CHARGING
BATTERY_CHARGE_STATE_DISCHARGING
External power is absent and the battery supplies the device. An absent field never carries that meaning.
BATTERY_CHARGE_STATE_CHARGING
The charging system reports that the battery is actively receiving charge.
BATTERY_CHARGE_STATE_CHARGED
External power is present and the implementation has evidence that charging completed. For a 4.2 V-class single-cell battery, measured voltage below 4.1 V is insufficient evidence of completion. A battery at 100 percent while operating without external power remains in BATTERY_CHARGE_STATE_DISCHARGING.
BATTERY_CHARGE_STATE_NOT_CHARGING
External power is detected, but the charging system does not report active charging and the implementation cannot substantiate charge completion. This state does not imply that the external source alone supplies the device.

The property contains live, read-only state. It is never included in a saved snapshot and is not changed by CMD_RESTORE. A device MAY emit unsolicited CMD_PROP_IS updates when the reported snapshot changes. Such updates SHOULD be coalesced or rate-limited so that measurement noise does not produce excessive ULCP traffic.

Battery diagnostics

Properties 4944–4956 are optional extensions to CAP_BATTERY. No additional capability is allocated. A device may implement any subset, except that raw status properties require PROP_BATTERY_GAUGE_FORMAT. Clients discover support by reading the properties individually or with CMD_PROP_MULTI_GET.

All are single-value, read-only Device Domain properties, with no asynchronous updates. Their values are acquired on request and are excluded from saved state. They do not change the gauge or charger. PROP_BATTERY retains its existing encoding and notification behavior.

PROP 4944: PROP_BATTERY_CURRENT

  • Value Type: Minimal signed LE integer, 1–4 octets, mA, or empty

Signed battery current: positive into the battery, negative out, zero is a valid reading.

PROP 4945: PROP_BATTERY_REMAINING_CAPACITY

  • Value Type: Minimal unsigned LE integer, 1–4 octets, mAh, or empty

Gauge’s current remaining-capacity estimate.

PROP 4946: PROP_BATTERY_FULL_CAPACITY

  • Value Type: Minimal unsigned LE integer, 1–4 octets, mAh, or empty

Gauge’s current estimate of capacity at full charge.

PROP 4947: PROP_BATTERY_DESIGN_CAPACITY

  • Value Type: Minimal unsigned LE integer, 1–4 octets, mAh, or empty

Design capacity configured in the gauge or board profile.

PROP 4948: PROP_BATTERY_EXT_POWER_PRESENT

  • Value Type: BOOL, or empty

External power detected by the platform, such as USB/VBUS, a DC input, or solar input; this does not imply battery charging or identify the source.

PROP 4949: PROP_BATTERY_PRESENT

  • Value Type: BOOL, or empty

Battery detected by hardware.

PROP 4950: PROP_BATTERY_GAUGE_FULL

  • Value Type: BOOL, or empty

Gauge’s full-charge indication, independent of charger completion.

PROP 4951: PROP_BATTERY_GAUGE_INITIALIZED

  • Value Type: BOOL, or empty

Gauge initialization has completed; this does not assert successful capacity learning or calibration.

PROP 4952: PROP_BATTERY_GAUGE_SMOOTHING

  • Value Type: BOOL, or empty

Gauge is currently applying smoothing to its reported capacity.

PROP 4953: PROP_BATTERY_CHARGE_VOLTAGE_REQUEST

  • Value Type: Tagged voltage request, below, or empty

Gauge’s requested charging voltage, distinct from measured voltage and the charger’s configured voltage limit.

PROP 4954: PROP_BATTERY_GAUGE_FORMAT

  • Value Type: PUI, or empty

Identifies the interpretation of the two raw status properties.

PROP 4955: PROP_BATTERY_GAUGE_STATUS

  • Value Type: Minimal unsigned LE integer, 1–4 octets, or empty

Raw gauge battery-status flags, interpreted using the format identifier.

PROP 4956: PROP_BATTERY_GAUGE_OPERATION_STATUS

  • Value Type: Minimal unsigned LE integer, 1–4 octets, or empty

Raw gauge operation-status flags, interpreted using the format identifier.

Compact value encodings

Senders MUST use the fewest octets that represent each diagnostic integer value. The property value’s existing length supplies the width; no additional width prefix or continuation bits are added to these signed or unsigned LE integers. This applies equally to individual reads and entries in a multi-get response.

  • Signed current: little-endian two’s complement, with a signed 32-bit range. Reuse the encoding of PROP_IDENT_ALTITUDE, requiring minimal output here. One octet covers −128 through 127; two cover −32768 through 32767; three cover −8388608 through 8388607. Keep a sign octet when removing it would change the value’s sign.
  • Unsigned capacities, voltage, and raw flags: little-endian with an unsigned 32-bit range. Remove high zero octets, retaining one octet for zero. One octet covers 0–255, two 0–65535, and three 0–16777215. Bit 7 of the last octet is ordinary data, not a sign or continuation bit.
  • Booleans: exactly one octet, 00 or 01.
  • Gauge format and voltage discriminator: retain PUI encoding, using its shortest form. Format 1 and either defined discriminator occupy one octet.

Receivers accept integer widths of one through four octets, sign-extending current and zero-extending unsigned values, including padded representations. Senders must not emit that padding. More than four octets is malformed. An empty property value remains unavailable; numeric zero is 00, never empty. False likewise represents an actual indication, never an unavailable reading.

Examples below show value bytes in hexadecimal, in transmission order:

ValueBytesOctets
Current −100 mA9C1
Current −200 mA38 FF2
Current −128 / −129 mA80 / 7F FF1 / 2
Current 127 / 128 mA7F / 80 001 / 2
Current 0 mA or capacity 0 mAh001
Capacity 200 mAhC81
Capacity 1500 mAhDC 052
Raw operation flags 0x00A6A61

Capacities are charge quantities in mAh, not energy in mWh. No host should replace the reported percentage with a ratio it calculates from capacities; it may display that ratio separately for diagnosis.

The charge-voltage-request value begins with a PUI discriminator:

CodeFollowing bytesMeaning
0Minimal unsigned LE integer, 1–4 octets, mVA specific voltage request.
1NoneRequest the charger’s maximum voltage.

For code 0, all remaining bytes encode the voltage: one through four are required, with the compact unsigned rules above. Code 0 without a voltage or with more than four voltage octets is malformed. Code 1 has no following bytes; any trailing bytes are malformed. A 4200 mV request is 00 68 10 (three octets total), and a maximum-voltage request is 01 (one octet).

An empty property value means unavailable, as below. Unknown discriminators are malformed. This avoids exposing a hardware sentinel as an implausibly large voltage. Reading this property never applies the request to the charger.

Support, availability, and errors

Use existing ULCP status codes with explicit meanings:

ConditionResponse to a diagnostic property read
This hardware/firmware does not implement the propertySTATUS_PROP_NOT_FOUND.
Field implemented, but no meaningful value in the observed stateSuccessful CMD_PROP_IS with an empty value.
Acquisition attempted and failed, including an I2C error or timeoutSTATUS_FAILURE.
A valid measurement or indication existsSuccessful CMD_PROP_IS containing the typed value.

For example, an initializing gauge may report GAUGE_INITIALIZED=false while its capacity estimate is empty. A detected battery removal is BATTERY_PRESENT=false; dependent measurements may then be empty. A missing presence detector is STATUS_PROP_NOT_FOUND, not false. A failed read must not masquerade as battery removal, initialization, or an empty successful measurement.

There is no separate support-discovery table: clients can discover individual support with their first multi-get and omit STATUS_PROP_NOT_FOUND fields from subsequent polls. That result is fixed for a firmware/hardware configuration; rediscover after reconnecting or updating firmware. An empty value or acquisition failure does not mean unsupported and must not cause the client to drop that property from future polls.

Within a multi-get, each result occupies its normal position. A failed gauge read need not suppress a successful external-power reading from the charger. Correlated fields that depend on the failed read all report failure. PROP_BATTERY retains its existing rules, including its own failure behavior; the diagnostic properties do not redefine its empty or omitted-field meanings.

Mutations (SET, INSERT, REMOVE) return STATUS_INVALID_ARGUMENT for implemented properties and STATUS_PROP_NOT_FOUND for unsupported properties. Reads do not unseal, reset, calibrate, configure, or otherwise change the gauge or charger. This restriction applies to the scalar diagnostics (4944–4956); the separate configuration snapshot permits temporary access unlocking.

Shared acquisition within a multi-get

The battery read group contains PROP_BATTERY and properties 4944–4958. A device implementing any diagnostic property follows these rules for the supported members of that group. Unsupported members retain their STATUS_PROP_NOT_FOUND results and require no acquisition:

  1. A single-property read acquires the requested live reading when serviced.
  2. A multi-get containing battery-group properties acquires all requested live battery values in one pass when that group is serviced. All occurrences of those properties, including duplicate keys, use that pass’s results.
  3. Only the requested measurements and their dependencies need to be read. Reading external-power presence alone must not require successful gauge communication.
  4. Semantic flags and raw status words come from the same register reads. The device must not reread a status word separately for each derived flag.
  5. The sample is scoped to that request. It is discarded on completion, cancellation, or session reset; an unrelated later read must not reuse it. Concurrent requests may share an acquisition that is already in flight.

This is a battery-group guarantee, not general multi-get atomicity. Registers are often read sequentially and may update internally during acquisition; one pass does not promise a hardware-latched, simultaneous sample. Board adapters should use available latching or bounded consistency checks where required by their gauge, and report failure if they cannot obtain a usable result.

Metadata such as gauge format may come from the fixed board configuration. Read-only design capacity may be configured or learned inside the gauge; it must not be confused with a writable ULCP setting. Battery values are excluded from saved state, and save/restore does not roll telemetry back.

Over Node Management, a retransmitted battery read is executed again like any other read, and takes a new sample. A read too large for one payload is still one acquisition: its continuations are cut from the retained response.

Raw gauge formats and the Pager mapping

Format 1 is allocated to the TI BQ27220: GAUGE_STATUS contains BatteryStatus (0x0A) and GAUGE_OPERATION_STATUS contains OperationStatus (0x3A), each represented as a compact unsigned integer. A BQ27220 raw word therefore occupies one or two octets, with its high zero octet omitted. Format 0 is reserved; other formats need a documented allocation before a device emits them. Raw-register implementations must also implement GAUGE_FORMAT.

Clients can show unknown format identifiers and raw words as numbers, while continuing to use the generic measurements and flags. They must not decode unknown raw formats as BQ27220 bits. Raw flag bits are opaque protocol data; reserved hardware bits are preserved rather than rejected by the ULCP codec.

The Pager adapter uses current (0x0C), remaining (0x10), full (0x12), design (0x3C), and requested voltage (0x30). BatteryStatus supplies presence (bit 3) and full (bit 9); OperationStatus supplies initialized (bit 5) and smoothing (bit 6). Requested voltage 0xffff maps to request code 1. These mappings follow the BQ27220 technical reference manual, sections 2.7–2.10 and 2.23–2.28.

On the Pager, external-power presence maps to USB/VBUS detection by the BQ25896; charger state also comes from the BQ25896. Preserve the distinction between charger completion, gauge full, and 100% state of charge. Their disagreement is useful diagnostic information.

PROP 4957: PROP_BATTERY_GAUGE_CONFIG

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: Optional with CAP_BATTERY; no separate capability
  • Value Type: PUI format, PUI schema version, format-specific fields
  • Post-Reset Value: Acquired on demand; never saved

A chip-specific inspection of the gauge’s live configuration. Ordinary battery polling does not imply reading this property. Hosts should request it explicitly for diagnosis and omit it from routine monitoring. Unsupported devices return STATUS_PROP_NOT_FOUND; acquisition or access-restoration failures return STATUS_FAILURE, never a partial snapshot. Implemented mutations return STATUS_INVALID_ARGUMENT.

An implementation may temporarily unlock gauge access to read configuration, but MUST restore its original access state before reporting success. It MUST NOT reset the gauge, enter a configuration-update mode, commit parameters, calibrate, or change learned capacity. Cleanup must finish even if the host disconnects or abandons the request. Other drivers must not interleave commands to the same gauge during inspection. Implementations bound hardware waits and validate returned memory addresses and checksums. The BQ27220 requires FULL_ACCESS for these RAM reads; merely unsealing it is insufficient. Entering FULL_ACCESS is distinct from entering CFGUPDATE. Before each security-key sequence, leave the gauge unaddressed for four seconds, including by ordinary telemetry polling, as described in TI’s security-mode timing guidance. Other devices on the same bus can continue operating. This delay also applies to fallback keys and restoration of an originally unsealed state.

The property participates in the battery acquisition group when requested, including duplicate-key reuse. Scalar status words describe their acquisition before temporary unlocking; they must not expose a transient access state as the steady state. Configuration fields are sequential reads, not an atomic hardware snapshot. Reading this property can take longer than scalar telemetry; hosts should allow a thirty-second device processing budget, plus transport time. A successful sealed-gauge inspection normally takes about nine seconds; ordinary scalar polling resumes after inspection and access restoration finish.

Format 1, version 1 describes the BQ27220 RAM parameter area identified by TI as CEDV Profile 1, plus shared configuration and calibration parameters. It does not select a profile. The battery-ID field is reported verbatim. Every field below appears exactly once, in order, without field IDs or lengths. Integer fields use shortest-form PUI. The two F4 fields instead use exactly four little-endian octets holding the chip’s raw calibration bit pattern; hosts must not assume IEEE-754 encoding. These are converted from the gauge’s big-endian memory representation. Unknown formats or versions must not be decoded using this layout. Truncation, extra fields, or values exceeding the indicated hardware width are malformed. Version 1 contains 48 fields and is at most 148 octets, including the two header PUIs.

OrderFieldRAM addressHardware octetsUnits / representation
1battery_id0x929A1bits
2operation_config_a0x92062bits
3operation_config_b0x92082bits
4cedv_config0x929B2bits
5full_capacity0x929D2mAh
6design_capacity0x929F2mAh
7design_voltage0x92A32mV
8termination_voltage_margin0x92A52mV
9charging_voltage0x91FD2mV
10taper_current0x92012mA
11discharge_detection0x92282mA
12charge_detection0x922A2mA
13quit_current0x922C2mA
14battery_low0x925120.01%
15learning_low_temp0x925B1raw
16overload_current0x92642mA
17near_full0x926B2mAh
18reserve_capacity0x926D2mAh
19charge_efficiency0x926F1%
20discharge_efficiency0x92701%
21fixed_edv00x92B42mV
22edv0_hold0x92B61s
23fixed_edv10x92B72mV
24edv1_hold0x92B91s
25fixed_edv20x92BA2mV
26edv2_hold0x92BC1s
27emf0x92A72raw
28c00x92A92raw
29r00x92AB2raw
30t00x92AD2raw
31r10x92AF2raw
32tc0x92B11raw
33c10x92B21raw
34age_factor0x92B31raw
35flag_config_a0x927F2bits
36flag_config_b0x92811bits
37full_set_voltage0x92882mV
38full_clear_voltage0x928A2mV
39full_set_soc0x928C1%
40full_clear_soc0x928D1%
41smoothing_config0x92711bits
42cc_gain0x91844F4 bits
43cc_delta0x91884F4 bits
44current_deadband0x91DE1mA
45cc_deadband0x91DF1294 nV
46voltage_0_dod0x92BD2mV
47voltage_50_dod0x92C72mV
48voltage_100_dod0x92D12mV

Addresses and hardware widths follow TI SLUUBD4A, table 3-2. Bitfields and raw values preserve the chip representation. In particular, the learning-temperature code is left raw; no temperature conversion is implied. Fixed EDV voltages must be interpreted alongside the compensation-enable bits and model coefficients. CEDV bit 8 is FCC_LIMIT, bit 4 selects independent charger learning, and bit 3 enables EDV compensation. Charge-termination voltage is a margin below the charging voltage, not an absolute cutoff.

Qmax and taper-window timing are not included: this schema exposes only the parameters with identified RAM locations in the cited memory map. It does not invent an address or substitute design capacity for an independently read Qmax.

PROP 4958: PROP_BATTERY_GAUGE_TELEMETRY

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: Optional with CAP_BATTERY; no separate capability
  • Value Type: PUI format, PUI schema version, format-specific readings
  • Post-Reset Value: Acquired on demand; never saved

Live chip-specific readings for monitoring over USB or Node Management. Reading this property does not unlock, configure, calibrate, or reset the gauge. It uses the ordinary battery acquisition timeout and shares the request’s acquisition with other battery properties, including repeated keys. It does not read PROP_BATTERY_GAUGE_CONFIG implicitly. Unsupported devices return STATUS_PROP_NOT_FOUND; a failed register read returns STATUS_FAILURE for this entire property, without suppressing other successful battery properties. Mutations return STATUS_INVALID_ARGUMENT when supported. No saved alias exists.

Format 1, version 1: BQ27220 standard commands

After format and version, a two-octet little-endian width bitmap precedes the 14 readings in the order below. Bit i corresponds to zero-based field i: clear means one value octet, set means two. Bits 14–15 are reserved and zero. Each value uses unsigned little-endian or signed two’s-complement little-endian encoding as specified below. Senders use the shortest width that preserves the value and sign; receivers accept either width. For example, −100 uses 9C, whereas −200 uses 38 FF. The bitmap supplies their lengths without a length octet per field. The complete value occupies 18–32 octets, including format, version, and bitmap. Missing fields, reserved width bits, and trailing data are malformed; unknown formats or versions must not be interpreted using this layout.

IndexFieldRegisterSignedUnit
0raw_coulomb_count0x22YesmAh
1temperature0x06No0.1 K
2internal_temperature0x28No0.1 K
3average_current0x14YesmA
4average_power0x24YesmW
5time_to_empty0x16Nominutes
6time_to_full0x18Nominutes
7standby_current0x1AYesmA
8standby_time_to_empty0x1CNominutes
9max_load_current0x1EYesmA
10max_load_time_to_empty0x20Nominutes
11cycle_count0x2ANocycles
12state_of_health0x2ENo%
13charging_current0x32NomA requested

These are raw standard-command readings, available while sealed. Read presence and initialization flags alongside them before treating them as battery estimates. The wire preserves all register bits, including sentinel values:

  • Time estimates of 65535 mean unavailable in the current charge/discharge state; they are not 65,535 minutes. Requested charging current 65535 means maximum requested current. Other fields retain their full numeric range.
  • RawCoulombCount is a signed 16-bit two’s-complement count: for example, register 0xFFEA means −22 mAh and encodes in one value octet, EA. It counts discharge upward and charge downward; the gauge clears it when it recognizes full charge. It is neither remaining capacity nor a lifetime charge counter. Do not assume continuity across gauge resets or the full-charge event when integrating samples.
  • Temperature is the configured gauging source. InternalTemperature is the IC’s internal sensor, not necessarily cell temperature. Celsius is raw / 10 − 273.15.
  • Current and power are positive during charge and negative during discharge. ChargingCurrent is a request, not measured current or charger configuration.
  • CycleCount uses the gauge’s configured accumulated-discharge threshold. StateOfHealth is its full/design-capacity ratio estimate, not independent evidence of cell condition. Neither resets merely because this property is read.

Register definitions follow TI SLUUBD4A, standard commands. Units in the table are TI’s documented register units, not a promise of calibrated measurement accuracy. The Pager’s observed AveragePower scale disagrees with the stated mW unit; see the hardware qualification notes. umshctl battery --gauge-telemetry --watch --json includes these readings in normal monitoring, retains raw values, labels unavailable estimates, and adds Celsius conversions. umshctl get battery-gauge-telemetry reads this property alone.

PROP 70: PROP_MAC_REPEATER_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_REPEATER
  • Value Type: BOOL
  • Post-Reset Value: Persisted

The first of the device-behavior settings (property identifiers 70–78), and the master switch for the repeater policy in PROP_MAC_REPEATER_REGIONS, PROP_MAC_REPEATER_DEFAULT_REGION, PROP_MAC_REPEATER_MIN_RSSI, and PROP_MAC_REPEATER_MIN_SNR, which are configurable while forwarding is disabled and take effect when it is enabled. When true, the device identity acts as an autonomous mesh repeater: its on-board node forwards the routable UMSH frames it receives—off the air, from an IP multicast group, or over backhaul—according to Repeater Operation and Nodes With a LoRa Radio, and it sets the repeater capability bit in its node identity. When false, the device identity forwards nothing on any medium—it sends only its own traffic and its attached host’s—and the bit is clear.

The capability bit is a statement of fact and MUST track the live forwarding state. The advertised role is a separate matter: it is configuration, set through PROP_IDENT_ROLE, and defaults to being derived from this flag rather than being fixed by it. A mobile repeater and a fixed tracker are both expressible.

This property governs only the forwarding behavior of the device identity. It is independent of PROP_MAC_PROMISCUOUS (a session-scoped host-delivery mode) and of the host identity, which never forwards.

The flag is device-domain state: it is part of the saved snapshot, so a CMD_SAVE arms an unattended repeater across power cycles, and it survives a change of host.

Asynchronous because a device MAY offer forwarding as a control the operator can reach—the menu on a device with a screen is where a user decides whether to spend their battery carrying other people’s traffic. A device that flips it locally MUST publish the new value like any other transition the host did not command.

Flood-contention tuning—the forwarding delay window, deferral count, and similar timing parameters—is not exposed; a repeater applies its local defaults.

PROP 71: PROP_IDENT

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: Signed node-identity payload

The device identity’s complete signed node identity: the canonical payload encoding—role, capabilities, and the descriptive options the device advertises— followed by its 64-octet detached EdDSA signature over that encoding.

This is the same statement the device makes over the air, in its standalone framing. A device MUST build it from the same values it would advertise in an Identity Request response, so a host reading it locally and a peer hearing it on the mesh cannot disagree about what the device is. It differs from that response in exactly two ways, both structural: it carries no request nonce, and it is authenticated by the signature rather than by an enclosing authenticated unicast.

The contents are nonce-free. They carry the Unix Timestamp when the device knows the time, so each read is signed afresh; reading this property is an operator-scale event.

What the device advertises can include where it is, which is right for a statement heard now and wrong for one kept. An identity meant to be kept— in a QR code, a contact link, a contact exchange—is PROP_IDENT_CONTACT.

PROP 72: PROP_IDENT_ROLE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: UINT8, or empty
  • Post-Reset Value: Empty, or restored from saved state

The ROLE byte the device identity advertises (see Node Primary Role).

An empty value—the factory default—means the device derives the role from what it is actually doing. While PROP_MAC_REPEATER_ENABLED is set, it derives Bridge if the device node also has a link beyond its radio—PROP_MAC_BACKHAUL is set, or the bridge client is enabled with a host and server key—and Repeater if it does not. Otherwise it derives Tracker, whatever links it has. On a device whose device identity is a messaging identity (see The Device Identity), it derives Chat whether or not the device forwards. Any other value is advertised verbatim.

Role and forwarding are deliberately separate. Forwarding is a fact, reported through the repeater capability bit; the role is how the device presents itself, which is the operator’s choice. Deriving it by default keeps the common cases right without a configuration step, and setting it explicitly expresses the ones derivation cannot reach—a repeater that is also mobile, a fixed node that is not a repeater.

Tethering does not appear here, or anywhere in a node identity. Whether some host is currently attached over the local control link is a transient local relationship, not a durable characteristic of the node, and the mesh has no business knowing it.

PROP 73: PROP_IDENT_MOBILE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: BOOL
  • Post-Reset Value: 1 (true) on a device whose device identity is a messaging identity, 0 (false) on any other; or restored from saved state

Whether the device identity advertises the mobile capability bit: true for a device that moves, false for one installed in a fixed location. A device with a messaging identity is carried by its person, so it starts out mobile.

Orthogonal to PROP_IDENT_ROLE and to PROP_MAC_REPEATER_ENABLED, and orthogonal to whether a host is tethered. A hand-carried repeater and a pole-mounted sensor are both ordinary configurations.

PROP 74: PROP_MAC_REPEATER_REGIONS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_REPEATER
  • Item Form: 1–24 octets of UTF-8 (the region’s string form)
  • Remove Selector: the region string
  • Post-Reset Value: Empty, or restored from saved state

The set of regions the device flood-forwards for, each held in its string form—the short code for a region that has one, the region name otherwise—the same form the Supported Regions identity option carries. From each string the device derives the 2-octet region code; the derived codes are the filter applied at the region-policy step of the forwarding procedure: a flood packet carrying region codes is forwarded only if at least one of them matches. An empty set—the factory default—imposes no regional restriction, so a tagged packet is forwarded whatever its region.

A device MUST reject an item that is empty or longer than 24 octets with STATUS_INVALID_ARGUMENT, and MAY reject a write that exceeds the number of entries it can hold.

Items compare without ASCII case, as their codes derive, so the set holds at most one entry per region. An insert whose item differs from a held one only in case respells that entry in place and reports the item as inserted; an item identical to a held one reports STATUS_ALREADY. The remove selector matches the same way, and a whole-value write keeps the last spelling it carried. Stored items keep the capitalization written.

A device with forwarding enabled and a non-empty set SHOULD advertise the same strings in its node identity, subject to that option’s count and packet-fit limits, so that a peer choosing a route can see what a repeater will carry. A device that is not forwarding makes no such claim and omits the option.

Whether an untagged packet is tagged on the way out is a separate decision, governed by PROP_MAC_REPEATER_DEFAULT_REGION.

PROP 75: PROP_MAC_REPEATER_DEFAULT_REGION

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_REPEATER
  • Value Type: One 2-octet region code, or empty
  • Post-Reset Value: Empty, or restored from saved state

The region code the device inserts into a flood packet that carries none, as permitted at the region-policy step of the forwarding procedure. An empty value—the factory default—means the device never tags: untagged packets are forwarded untagged. Any other value MUST be exactly two octets; a device rejects other lengths with STATUS_INVALID_ARGUMENT.

Tagging is opt-in because it is a claim about where the packet is, not merely about where the repeater is willing to forward. A repeater that filters on a region list without asserting one leaves the decision to whoever originated the packet.

The configured code SHOULD be one of the codes in PROP_MAC_REPEATER_REGIONS when that list is non-empty, so that the repeater will itself forward what it tags. A device does not enforce this across the two writes, and the two properties may be set in either order.

Only untagged packets are affected: an already-tagged packet is forwarded with its codes unchanged, and a second code is never added.

PROP 76: PROP_MAC_REPEATER_MIN_RSSI

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_REPEATER
  • Value Type: INT16 in dBm, or empty
  • Post-Reset Value: Empty, or restored from saved state

The weakest signal the device will flood-forward, in dBm. An empty value—the factory default—imposes no threshold. Any other value MUST be exactly two octets.

The threshold is the repeater’s half of step 7 of the forwarding procedure: where the packet also carries a minimum, the higher of the two applies. Raising it trades reach for a quieter mesh, which is what a dense deployment wants from a repeater sitting at the edge of everyone’s range.

Applies to flood forwarding only, MeshCore floods included. Source-routed packets are forwarded on the strength of the route, not the link.

PROP 77: PROP_MAC_REPEATER_MIN_SNR

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_REPEATER
  • Value Type: INT8 in whole dB, or empty
  • Post-Reset Value: Empty, or restored from saved state

The lowest signal-to-noise ratio the device will flood-forward, in whole dB. An empty value—the factory default—imposes no threshold. Any other value MUST be exactly one octet.

The threshold is the repeater’s half of step 8 of the forwarding procedure, combined with any packet-imposed minimum the same way PROP_MAC_REPEATER_MIN_RSSI is. On spreading factors that decode well below the noise floor, this is the more meaningful of the two thresholds.

Applies to flood forwarding only, MeshCore floods included.

MeshCore forwarding

A device with CAP_MESHCORE_FORWARDER also forwards MeshCore v1 packets, interoperating with MeshCore repeaters and companions as a MeshCore repeater would, with MeshCore repeater firmware v1.17.1 as the compatibility baseline. The two protocols share the radio, the duty ledger, and the link-quality thresholds; their packets are told apart by the version bits of the first octet, so neither is mistaken for the other.

The MeshCore forwarder runs only while all of these hold:

PHY enabled AND MESHCORE_FORWARDING_ENABLED
            AND running identity prefix not reserved

PROP_MAC_REPEATER_ENABLED governs UMSH forwarding alone; a device may forward either protocol without the other.

The forwarder takes its path entries from the public key of the device identity it was brought up under; it signs and originates nothing. A MeshCore node never runs under a key whose first octet is 0x00 or 0xFF, so a device whose running identity has such a key does not forward MeshCore traffic, whatever its settings. A client can apply the same test to PROP_DEV_KEY.

Whenever the gate is false, the forwarder discards everything it had seen and queued.

MeshCore regions are configured in PROP_MESHCORE_REGIONS. PROP_MAC_REPEATER_REGIONS and PROP_MAC_REPEATER_DEFAULT_REGION apply to UMSH only.

PROP 4976: PROP_MESHCORE_FORWARDING_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_MESHCORE_FORWARDER
  • Value Type: BOOL
  • Post-Reset Value: False, or restored from saved state

Whether the device forwards MeshCore traffic. Independent of PROP_MAC_REPEATER_ENABLED.

Changing it from false to true MUST fail with STATUS_INVALID_STATE while the first octet of PROP_DEV_KEY is 0x00 or 0xFF. Replacing the device identity leaves the value unchanged, and a write that leaves it unchanged succeeds.

PROP 4977: PROP_MESHCORE_REGION_POLICY

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_MESHCORE_FORWARDER
  • Value Type: UINT8
  • Post-Reset Value: 0, or restored from saved state

How MeshCore transport codes gate flood forwarding. A scoped flood matches when its first transport code is the one a region in PROP_MESHCORE_REGIONS gives it.

ValuePolicyForwarded floods
0OpenUnscoped floods, and scoped floods that match
1Scoped onlyScoped floods that match
2IgnoreEvery flood, whatever its scope

A device MUST reject any other value with STATUS_INVALID_ARGUMENT. Direct traffic is never gated by region.

PROP 4978: PROP_MESHCORE_REGIONS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_MESHCORE_FORWARDER
  • Item Form: 1–30 octets of UTF-8 (a MeshCore region name without its #)
  • Remove Selector: the region name
  • Post-Reset Value: Empty, or restored from saved state

The MeshCore public regions whose scoped floods the device forwards. Each region’s transport key is the first 16 octets of SHA-256 over # followed by the item, as MeshCore derives a hashtag region’s key. The derivation is case-sensitive, so items compare octet for octet: us-ca and US-CA are two regions. An insert identical to a held item reports STATUS_ALREADY, and the remove selector matches the same way.

A device MUST reject an item that is empty, longer than 30 octets, not UTF-8, or begins with # or $ with STATUS_INVALID_ARGUMENT, and MAY reject a write that exceeds the number of entries it can hold. A name beginning with $ is a MeshCore private region, whose key does not derive from its name.

PROP 78: PROP_DEV_DISCOVERABLE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether the device identity answers Identity Requests addressed to it, including broadcast solicitations whose filters select it. When false, the device identity ignores every Identity Request.

Discoverability defaults on: a deployed device is infrastructure, and being askable is most of what makes it administrable in the field. The property is the opt-out for deployments where the device should not volunteer its identity to arbitrary nearby askers.

Affects only Identity Request responses. Unsolicited advertisements and beacons are governed by the advertisement policy, and the device’s participation in forwarding by PROP_MAC_REPEATER_ENABLED; neither is changed by this property. A device that is not discoverable still advertises on its own schedule if it has one—declining to answer strangers and declining to speak are different decisions.

PROP 79: PROP_ALERT

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_ALERT
  • Value Type: PUI
  • Post-Reset Value: 0 (ALERT_NONE)

What the device is currently doing to draw a person’s attention to where it physically is. A radio that has been set down in the wrong place is found by making it announce itself.

ValueName
0ALERT_NONE
1ALERT_LOCATE
ALERT_NONE
The nominal state. The device draws no attention to itself beyond whatever its ordinary operation involves.
ALERT_LOCATE
The device makes itself as conspicuous as its hardware allows, and keeps doing so until the alert is cleared.

Values other than these are rejected with STATUS_INVALID_ARGUMENT.

The presentation is board-defined. The property carries intent, not presentation: a device with a buzzer sounds it, a device with only an indicator LED flashes it, a device with a display can say so on the screen. CAP_ALERT states that the device has some means of making itself conspicuous and nothing more, so a host MUST NOT assume that an alert is audible, or that two devices alert alike. Because the alert runs unattended on a device that may already be low, it is expected to be intermittent rather than continuous, and it does not defer or inhibit a protective shutdown.

The alert overrides local quiet settings. A device whose buzzer has been silenced, through a local control or PROP_SOUND_ENABLED, still sounds ALERT_LOCATE: locating a misplaced radio is precisely the case that silencing must not defeat. The alert suspends the local setting rather than changing it, so clearing the alert leaves the device as quiet as it was before.

A device returns to ALERT_NONE three ways:

  1. The host writes ALERT_NONE.
  2. Local user input cancels it. A device with any user input at all MUST offer a way to cancel an alert from the device itself— whoever finds the radio is rarely holding the phone that set it off. The input that cancels performs none of its other functions, so that fumbling for a beeping radio cannot change its configuration; a deliberate gesture such as hold-to-power-off MAY remain reachable while an alert is active.
  3. The deadline expires. A device MUST bound how long it will remain in ALERT_LOCATE; a few minutes is RECOMMENDED. Writing ALERT_LOCATE while it is already in effect succeeds and restarts the deadline, which is how a host holds an alert open for a longer search.

PROP 80: PROP_ADVERT_INTERVAL

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_ADVERT
  • Value Type: UINT32
  • Post-Reset Value: 14400 (four hours), or restored from saved state

Seconds between unsolicited advertisements—broadcasts carrying the device’s node identity in a signed frame. Zero sends none.

An automatic advertisement is sent with no flood hops and no source route, so it reaches the nodes that can hear the device directly and stops there. It is the largest frame the device originates, and what it carries is a standing statement rather than news; repeating that statement across the whole mesh every interval would spend airtime out of all proportion to what a distant listener learns. A device that wants to be findable further away publishes a path with PROP_BEACON_INTERVAL instead, which costs a fraction as much.

Because the advertisement is a signed broadcast meant for listeners that may not hold the device’s key, it carries its source in full-key form (§Signature Usage).

A device MUST reject a non-zero interval outside 1200 seconds (twenty minutes) to 86400 seconds (twenty-four hours) with STATUS_INVALID_ARGUMENT. Neither bound is an airtime control—the duty limit is that—but the two ends fail differently. Below the floor a device spends the mesh’s airtime restating what it already said; above the ceiling the schedule has stopped being one, and zero says so more plainly.

The interval is a minimum rather than an exact cadence: each period is scattered later by a random fraction of it, never earlier, so the configured value is the shortest gap between two unsolicited announcements.

Scheduled sends are subject to the same duty accounting and channel access as any other transmission: a send the device cannot make when it falls due is skipped, not queued.

The schedule is not the only way an announcement goes out. CMD_ANNOUNCE sends one now, with a reach and a source form the host chooses; the two are independent, and neither disturbs the other’s timing.

PROP 81: PROP_BEACON_INTERVAL

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_ADVERT
  • Value Type: UINT32
  • Post-Reset Value: 3600 (one hour), or restored from saved state

Seconds between unsolicited beacons—broadcasts with no payload at all. Zero sends none.

A beacon is sent with a flood budget and the Trace option, so what arrives at a distant node is a usable path back to the device and the signal quality of every hop along it. What it does not carry is any statement of who the device is: that is what an advertisement is for, and a listener that has never met this device learns only that something with a given source hint is reachable.

The two intervals are independent because they announce different things at very different costs. A mesh usually wants the path refreshed often and the identity restated rarely.

The accepted range, the per-period scatter, and the duty accounting are as described for PROP_ADVERT_INTERVAL.

PROP 82: PROP_STARTUP_BEACON

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_ADVERT
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether the device emits one beacon once it has come up. On by default: a node that has just restarted is exactly the node whose neighbors hold the stalest paths to it, and a single empty broadcast is the cheapest correction available.

The beacon is emitted after the device’s own configuration has been applied, so it reflects the device as it will actually run rather than as it booted. Unlike a scheduled period it is not scattered: devices do not restart in unison, so bring-up is already spread out by whatever staggered it.

PROP 83: PROP_IDENT_LOCATION

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_IDENT
  • Value Type: 0–7 octets
  • Post-Reset Value: Empty, or restored from saved state

The position the device identity carries in its node identity, in the variable-precision location encoding. An empty value means the identity advertises no position.

This property is where the advertised position comes from, whatever put it there. A fix writes it when PROP_GNSS_IDENT_UPDATE is set; a host or administrator writes it when that is clear. There is one advertised position and one place to read it.

Required by CAP_IDENT rather than CAP_GNSS, because where a node is and whether it can determine that itself are different questions. A fixed repeater on a hilltop has a position worth advertising and no receiver to find it with, and a host placing that node needs somewhere to put the coordinates.

The value’s length is its precision, so a written position needs no separate precision setting: a host that wants to advertise a neighborhood rather than an address writes a shorter value. PROP_GNSS_IDENT_PRECISION governs what the device clamps its own fixes to and does not constrain a written value.

A write is refused with STATUS_INVALID_STATE while PROP_GNSS_IDENT_UPDATE is set. The device is maintaining the value from its own fixes, and a written position would survive only until the next one—a setting that silently reverts is worse than one that refuses. A host that means to place the node clears auto-update first.

Devices SHOULD announce this property when it changes. Unlike PROP_GNSS_LOCATION it moves only when the advertised identity does, which is already rate-limited by the clamping precision, so announcing it costs what it is worth.

PROP 84: PROP_IDENT_ALTITUDE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_IDENT
  • Value Type: 1–4 octets, or empty
  • Post-Reset Value: Empty, or restored from saved state

The altitude the device identity carries, in meters above the WGS-84 ellipsoid—the same units and reference as the node identity’s altitude option. Writable and refused under exactly the conditions described for PROP_IDENT_LOCATION.

The value is a two’s-complement signed integer, little-endian, in the fewest octets that hold it: an altitude of 100 m occupies one octet, 200 m two, and the range extends to four. Most nodes are within a byte of sea level, and this property is read over a link where a byte is worth saving. Negative values are ordinary—a node below the ellipsoid, which much of the world’s dry land is.

A device MUST accept any length from one through four and sign-extend it, so a host that pads to a fixed width is understood. A device SHOULD report the minimal form.

An empty value means no altitude is advertised. A device that advertises no location advertises no altitude either: an altitude alone places nothing.

PROP 85: PROP_IDENT_CONTACT

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: Signed node-identity payload

The device identity’s signed node identity as a statement to be kept: in a QR code, a contact link, or a contact exchange. It is built from the same role, capabilities, and options as PROP_IDENT, with these left out:

  • Node Location (option 1) and Altitude in Meters (option 2), when the identity carries the mobile capability bit. A kept position goes stale and travels wherever the contact does; a node that does not move keeps its position, which is worth handing out.
  • Supported Flood Regions (option 4). They are forwarding policy, which changes, not part of who the node is.
  • Nonce (option 5), which no standalone identity carries.

It carries the Unix Timestamp when the device knows the time, which is what lets whoever holds it judge how old it is. Like PROP_IDENT, it is signed afresh on each read.

PROP 88: PROP_GNSS_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_GNSS
  • Value Type: BOOL
  • Post-Reset Value: device-defined, conventionally 0 (false), or restored from saved state

Whether the GNSS receiver is powered.

Asynchronous because a device MAY offer the receiver as a control the operator can reach—a button, a menu entry—and a switch someone can flip is a value that moves without the host asking. A device that flips it locally MUST publish the new value like any other transition the host did not command.

False means the lowest power state the board can put the receiver in, not merely an idle one: on a battery-powered node the receiver is typically the largest continuous load there is, and a property that only stopped reporting would be a property that solved nothing.

The post-reset value is the device’s to choose, and it SHOULD be false: a device that has never been told to care where it is should not be spending a battery finding out. A device whose purpose is to know where it is—a fixed outdoor node with a panel rather than a pocket tracker on a cell—MAY default it true instead, and SHOULD document that it does. Either way the value is only a starting point: saved state overrides it in both directions, and a host that wants a particular state sets it rather than assuming one.

Disabling the receiver does not clear the wall clock. Time already obtained stays as good as the device’s oscillator keeps it, which is the whole point of having acquired it.

One exception is permitted, and only for boards where the receiver’s own real-time-clock domain is the only clock the board has: that domain MAY remain powered while this property is false, and the device MAY briefly power the receiver at boot to read the time back out of it. That is a clock operation, not a positioning one—position data observed during it is discarded, and it is governed by PROP_GNSS_TIME_TRUST rather than by this property.

PROP 89: PROP_GNSS_LOCATION

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_GNSS
  • Value Type: 0–7 octets
  • Post-Reset Value: Empty

The position of the most recent fix, in the variable-precision location encoding—the same nibble-interleaved grid code node identities carry, so a host never has to convert between two position formats.

An empty value means no fix has been obtained since the receiver was last powered. The device reports the position it actually has rather than the last one it remembers across a power cycle: a position that was true somewhere else is worse than no position at all.

The length is the device’s own honest precision for that fix and MAY vary between reads. A host MUST NOT read more precision into a value than its length carries; the encoding’s truncation property means a shorter value is a correct lower-precision statement of the same position, never a different one.

A device MUST generally avoid announcing this property, and a host that wants a position MUST be prepared to read one. A receiver produces a fix about once a second, and at fine precision the readings of a receiver standing perfectly still still differ from each other, so a device that published every change would transmit continuously—and wake its host every time—on behalf of a host that may not be looking. No threshold rescues this: the one that would be quiet enough to be worth having is coarse enough that the announcements it does send are too late to be the point.

A host SHOULD nonetheless accept an announcement that arrives. The value carries what a read would have returned, a device may have its own reason to volunteer one, and a host that treats it as a protocol error gains nothing for the strictness.

Read-only. This property reports what the receiver sees, and nothing a host writes could make that true. A position placed by hand belongs to PROP_IDENT_LOCATION, which is what the identity advertises and is writable for exactly that reason.

PROP 90: PROP_GNSS_ALTITUDE

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_GNSS
  • Value Type: INT32_LE, or empty
  • Post-Reset Value: Empty

Altitude of the most recent fix, in meters, in the same units and reference as the node identity’s altitude option, so the two are directly interchangeable.

Empty when there is no three-dimensional fix—including while the receiver holds a two-dimensional one, which has a position but no altitude. Read-only for the same reason as PROP_GNSS_LOCATION.

PROP 91: PROP_GNSS_FIX

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_GNSS
  • Value Type: UINT8
  • Post-Reset Value: 0

The quality of the current position solution.

ValueMeaning
0No fix
1Two-dimensional fix—position without altitude
2Three-dimensional fix

Unlike the three properties that describe a position, this one is never empty: a device that is not fixed knows it is not fixed, so it reports 0. A receiver that is switched off reports 0 for the same reason. This is the distinction the whole positioning surface rests on—zero for the facts the device is sure of, empty for the position it does not have.

This is the positioning property a device SHOULD announce, and the reason it is the exception to PROP_GNSS_LOCATION’s silence is that it is not a measurement. It changes when the receiver acquires or loses a solution, which is a few times in a session rather than a few times a second, and it is what tells a host whether reading a position is worth anything at all.

PROP 92: PROP_GNSS_PRECISION

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_GNSS
  • Value Type: UINT16_LE, or empty
  • Post-Reset Value: Empty

Estimated horizontal accuracy of the current fix, in decimeters. Empty when there is no fix.

An estimate, not a measured error bound. Receivers generally report a dilution of precision, which becomes a distance only after multiplying by an assumed range error; a device that has a real accuracy figure SHOULD report that instead. Hosts MUST treat the value as indicative and MUST NOT present it as a guarantee.

Deliberately distinct from the length of PROP_GNSS_LOCATION: that is how precisely the device is willing to say where it is, and this is how precisely it knows. The two move independently.

PROP 93: PROP_GNSS_SATELLITES

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_GNSS
  • Value Type: UINT8, optionally followed by a second UINT8
  • Post-Reset Value: 0

The number of satellites contributing to the current solution, optionally followed by the number the receiver can see at all.

A device that cannot distinguish the two reports only the first octet. As with PROP_GNSS_FIX, a receiver that is off or searching reports 0 rather than the empty value.

Chiefly a diagnostic: it is what distinguishes an antenna fault from a sky that is simply obstructed, which is otherwise invisible to anyone not holding the device.

PROP 94: PROP_ILLUMINANCE

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_ILLUMINANCE
  • Value Type: UINT32_LE, or empty
  • Post-Reset Value: Not applicable

The ambient illuminance at the device, in millilux.

Millilux rather than lux because the readings that matter are at the dark end: a full moon is around 0.3 lux and starlight two orders of magnitude below that, so a device deciding how bright to make an indicator at night is working entirely inside what whole lux would round to zero. The unsigned 32-bit range still reaches past direct sunlight.

The value is a measurement taken when the property is read, not stored state, so a device samples on each get rather than answering from a cache. It follows that nothing about it is saved or reset: CMD_RST leaves it alone because there is nothing to leave.

An empty value means the device has no reading—the sensor did not answer, or is unavailable for as long as some other part of the device holds the hardware it shares. This is the same “we do not know” that PROP_TIME reports for an unset clock, and is not an error: a host that asked for the light level and got no answer has learned what it needed to.

A sensor that saturates reports its clamped maximum rather than an extrapolation past the point where it stopped responding to light. The alternative—a number derived from a transfer function outside the range it was fitted in—is indistinguishable at the host from a real reading.

Temperature sensors

CAP_TEMPERATURE implements both properties below, with no prerequisite capabilities. They are Single-Value, Read-Only properties whose values are ordered arrays. The multi-value property model describes unordered sets and cannot express their positional correspondence. Both support ordinary gets and multi-get processing, have no unsolicited updates, and have no saved aliases. Supported properties reject Set, Insert, and Remove with STATUS_INVALID_ARGUMENT; devices without the capability return STATUS_PROP_NOT_FOUND.

The environment owns an append-only inventory. Array position identifies a sensor. New sensors append to both arrays; existing entries MUST NOT be renamed, reordered, removed, or reassigned during operation. An unavailable or disconnected sensor retains its name and position. Labels need not be unique and MUST NOT be interpreted as sensor types. They may describe chip die, battery, or external sensors.

Session resets and CMD_RST preserve this inventory. A physical reboot may rebuild it; hosts MUST obtain fresh names rather than persist index associations across connections. Discovery uses CAP_TEMPERATURE, which may be advertised with no sensors registered. No protocol-version change or storage migration is required.

PROP 95: PROP_TEMPERATURES

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_TEMPERATURE
  • Value Type: Ordered array of UINT16_LE
  • Post-Reset Value: Fresh acquisition

Each word is a temperature in tenths of a kelvin, with no count or per-reading prefix. The value length determines the count. 0x0000 through 0xFFFE are numeric readings; 0xFFFF means unavailable, invalid, disconnected, or failed acquisition. An empty array means no sensors are registered. Odd-length values are malformed. For example, A5 0B FF FF reports 298.1 K at index 0 and an unknown reading at index 1.

Each fresh get samples all sensors registered when acquisition begins. Sensors appended during acquisition are included in the next get. Sampling need not be simultaneous. Failure of one sensor MUST NOT suppress other readings: each failed slot is 0xFFFF, and an all-failed acquisition returns the full array of unknown values. Transport retransmissions retain their existing retry semantics.

PROP 4876: PROP_TEMPERATURE_NAMES

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_TEMPERATURE
  • Value Type: Ordered array of PUI-length-prefixed UTF-8 names
  • Post-Reset Value: Unchanged

Each name is its UTF-8 byte length in PUI form followed by the bytes, without a terminator. Names contain 1–64 bytes and no NUL. An empty array is valid. Hosts validate the complete array before using any name. Reading names does not sample sensors or initiate hardware discovery.

Hosts should request temperatures and names together on the initial read, using multi-get. Once names are known, fetch temperatures and request names again only if a reading lacks a known name. Names may be reused within the connection; index associations must not persist across connections. Additional names represent sensors appended since acquisition began and have not yet been read.

While replacement names are loading, hosts retain known labels. Refused, malformed, or incomplete metadata must not erase known names or valid measurements. Validate the complete names encoding before replacing labels, use numbered labels for unmatched indices, and report metadata problems.

The reference sender supports 16 sensors and 272 encoded name bytes, and rejects capacity overflow with STATUS_NOMEM without truncation or reindexing. These are implementation capacities, not limits imposed on other devices by the wire decoders.

PROP 4866: PROP_TIME

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_TIME
  • Value Type: UINT32_LE, or empty
  • Post-Reset Value: Unchanged

The device’s wall clock, as seconds since the Unix epoch, UTC.

The count is unsigned, which puts the end of the representable range at 2106-02-07T06:28:15Z rather than at the 2038 rollover of the signed encoding. Nothing in this revision needs to handle a wrap.

An empty value means the device does not know what time it is. This is a normal state, not a fault: a device with no receiver, no battery-backed clock, and no host to ask has genuinely never been told. A device MUST report the empty value rather than an invented one, a zero, or a build timestamp.

A device that does not know the time MUST NOT display a clock, or any other indication of the current time, on any local user interface. A plausible-looking wrong time is worse than a blank space: an operator reads a displayed clock as a fact about the device, and a device with a screen is exactly the device somebody will trust.

Setting the property sets the clock. Setting the empty value returns the device to not knowing—the operator’s way of saying that whatever the device believes is wrong. A host-supplied time outranks every receiver-derived one, including while PROP_GNSS_TIME_TRUST is clear: the operator is the more authoritative source by definition.

The clock is not part of the saved snapshot. An epoch written to flash accumulates unbounded error while the device is off, so a clock is restored from a real time source—a receiver, a battery-backed real-time clock, or a host—or not at all. CMD_RST does not clear it; only an empty write does.

Devices announce this asynchronously. The transition from not knowing to knowing is the one worth announcing; a routine re-synchronization that agrees with the current value is not, and a device SHOULD NOT publish one.

PROP 4888: PROP_TZ_RULE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_TIME
  • Value Type: UTF-8, at most 64 octets
  • Post-Reset Value: UTC0, or restored from saved state

The local time zone, as a POSIX TZ rule (IEEE 1003.1 §8.3, with the extensions of RFC 8536 §3.3.1): PST8PDT,M3.2.0,M11.1.0, CET-1CEST,M3.5.0,M10.5.0/3, <+0530>-5:30. Local time at an instant is UTC plus the offset the rule gives for that instant, so the device keeps daylight saving time itself. A zone without daylight saving time is a rule without its daylight part.

Unlike PROP_TIME this always has a value. Where a device is meant to be is configuration and is known from the moment it is commissioned; what time it is is a measurement and may not be. Separating them is what lets a device render a local time the instant it acquires a clock, with no second round trip.

A rule describes a zone’s current practice, not its history. It is what the time zone database records at the end of each zone’s TZif file, which is how a host derives one from a zone identifier.

A value that is not a rule is rejected with STATUS_INVALID_ARGUMENT, as is one with an abbreviation longer than eight characters, or with an offset that is not a whole number of minutes or lies outside −12:00 through +14:00. Outside those, a value is a mistake, and a device that accepted one would display a confidently wrong local time.

Asynchronous because a device with a screen may offer the zone among its own settings (see Changes Made at the Device).

PROP 4889: PROP_TIME_24H

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_TIME
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether a clock the device shows writes hours from 0 to 23. Clear, it writes them from 1 to 12 with AM or PM. A device that shows no time keeps the value and does nothing with it.

Asynchronous for the same reason as PROP_TZ_RULE.

PROP 4868: PROP_GNSS_IDENT_UPDATE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_GNSS
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether position fixes refresh the location the device identity advertises in its node identity.

Off by default. Broadcasting where you are is a decision, and a device that started doing it because a receiver was switched on would be making that decision on the operator’s behalf.

When on, the device clamps each fix to PROP_GNSS_IDENT_PRECISION before advertising it. A device SHOULD act on a fix only when the clamped position actually changes, rather than on every fix: at a coarse precision a stationary node’s fixes all land in the same cell, and re-advertising each one spends airtime to say nothing.

Asynchronous because a device MAY offer this as a control the operator can reach, alongside the receiver switch it is deliberately separate from. A device that flips it locally MUST publish the new value like any other transition the host did not command.

Switching it off stops the updating and leaves PROP_IDENT_LOCATION and PROP_IDENT_ALTITUDE holding the last position written to them, which becomes an ordinary written value that a host may then change or clear. This is how a fixed node is placed without anyone reading coordinates off a screen: switch the receiver on, let it find where it is, switch auto-update off, and what it found stays.

The value is therefore a claim the operator is making rather than one the device is maintaining, and a node that moves afterward advertises where it was. Clearing the location is the way to stop advertising a position; a host that wants no stale claim writes an empty value.

Note that the Unix Timestamp option dates the identity payload, not the position—a node that has been stationary for a day still stamps each payload with the moment it was built.

PROP 4869: PROP_GNSS_IDENT_PRECISION

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_GNSS
  • Value Type: UINT8
  • Post-Reset Value: 5, or restored from saved state

How many octets of variable-precision location the advertised position is clamped to, 1 (coarsest) through 7 (finest).

The default of 5 is a cell of roughly 38 × 19 m at the equator: fine enough to place a node on a street, coarse enough not to place it in a room. That trade—not the receiver’s accuracy—is what this property exists to control, which is why it is separate from PROP_GNSS_PRECISION.

0 is rejected with STATUS_INVALID_ARGUMENT rather than read as “advertise nothing”: PROP_GNSS_IDENT_UPDATE is how the advertisement is switched off, and a precision that silently meant the opposite of a precision would be a trap. Values above 7 are rejected for the same reason the encoding stops there.

Writable while auto-update is off, so a whole positioning policy can be staged and enabled last.

PROP 4870: PROP_GNSS_TIME_TRUST

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_GNSS
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether time derived from the GNSS receiver may set the wall clock.

On by default: the sky is normally the best clock a device of this class has, and a node that sets itself needs no operator at all.

When clear, no receiver-derived time touches PROP_TIME—not a fix’s time, and not a read of the receiver’s own real-time clock at boot. This is the opt-out for a receiver whose time cannot be trusted: a jammed or spoofed sky can carry a plausible and badly wrong time, and a clock silently reset to it is worse than a clock that has stopped, because everything downstream will believe it.

Position reporting is unaffected. The two are separable, and an operator who distrusts the time may still want to know where the device thinks it is—including in order to notice that it is wrong.

Asynchronous for the same reason as PROP_TZ_RULE.

Every transition to ALERT_NONE that the host did not command MUST be reported with an unsolicited CMD_PROP_IS.

The deadline is the only bound. In particular, the alert is not cleared on detach: the link to the searching host drops as soon as the searcher walks out of range, which is the moment the alert becomes most useful. It is likewise unaffected by CMD_RST, which resets session state and not the physical behavior of the device.

The property is live device-domain state. It is never included in a saved snapshot, is not changed by CMD_RESTORE, and is ALERT_NONE after every reset—a device that loses power mid-alert comes back quiet.

Display motion wake

CAP_DISPLAY_MOTION_WAKE (59) means the device supports waking its display for movement qualified by screen-up orientation. It does not promise that the sensor is currently available. Sensor failures suppress automatic wake; normal device controls remain available.

PROP_DISPLAY_MOTION_WAKE_ENABLED

Property 4875; BOOL; device domain; Get, Set, and unsolicited CMD_PROP_IS notification when changed. Requires CAP_DISPLAY_MOTION_WAKE. The default is true on supported platforms. Unsupported platforms omit the capability and answer Get and Set with STATUS_UNIMPLEMENTED. Malformed values use the normal BOOL validation and return STATUS_INVALID_ARGUMENT.

This property enables the display’s motion policy. It does not control global accelerometer power: another consumer may still need movement detection when this property is false. It is included in CMD_SAVE, restored with the device configuration, and has a platform default of true after clearing saved state. CMD_RST follows the existing saved-state restore rules. A saved snapshot missing the property uses the platform default. A local toggle is saved as Changes Made at the Device describes.

The display wakes only from its fully lapsed state. Movement while active or dim consumes the episode without refreshing the attention timeout. Disabling the property cancels pending motion wakes. Explicit shutdown retains the platform’s existing button wake behavior.

The Pager requires fresh movement followed by at least 200 ms of consistently screen-up acceleration. Its orientation range extends to 45 degrees from horizontal with the keyboard lower than the screen, and 30 degrees sideways or in the opposite direction. Combined tilts share an elliptical angular allowance. Verification uses a bounded one-second window. Rearming requires two seconds of sensor-reported stillness, or an outside orientation interval with exit thresholds of 55 degrees in the reading direction and 40 degrees otherwise, followed by fresh movement and screen-up verification. Sensor reporting latency can make the necessary physical pause longer. Flat, screen-up pocket movement cannot be distinguished from a pickup by orientation alone. This version adds no raw motion telemetry, threshold controls, or GPS automation properties.

To suppress small vibrations, a native movement report must also be supported by a filtered acceleration-vector change of at least 0.12 g sustained for 80 ms within that verification window. The reference comes from its first three valid samples; small oscillations do not accumulate. This check belongs to the display policy and does not suppress native movement observations for other consumers. Exceptionally slow pickups, or pickups completed before verification begins, may not qualify. This does not identify every kind of bed or pocket movement.

Display and Keyboard

A device with a screen and keyboard of its own exposes their settings in the device domain, so they are saved with the rest of its configuration and reachable from wherever it is administered.

PROP 4877: PROP_DISPLAY_BRIGHTNESS

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_DISPLAY
  • Value Type: UINT8
  • Post-Reset Value: device-defined, or restored from saved state

The screen’s brightness. 255 is the brightest the screen goes, and 0 the dimmest level that is not off: brightness never turns a screen dark. On a screen that is readable unlit, such as e-paper under a front light, 0 turns the light off.

The levels in between are the device’s to map onto its screen, and it reports the value written.

PROP 4890: PROP_DISPLAY_TIMEOUT

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_DISPLAY
  • Value Type: UINT16
  • Post-Reset Value: device-defined, or restored from saved state

How long, in seconds, the screen stays lit after the last input while it is in use, before it goes dark. A device that dims its screen as a warning does so within this time, not after it. On a screen that is readable unlit, such as e-paper under a front light, it is how long until the light goes out and the screen returns to where it rests.

0 is never: no amount of time without input darkens the screen. The device still darkens it when asked to, such as by a control that locks it.

The value governs the screen in use, not a locked one: how long a locked screen stays lit is the device’s to decide, and is never longer than this value when this value is not 0.

A device keeps 0, or a value of 10 or more. A write of 1 to 9 is applied as 10, and answered with the value applied. A device that cannot keep its screen lit as long as a value asks applies the longest it can, and answers with that.

PROP 4878: PROP_KEYBOARD_LIGHT

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_KEYBOARD
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether the keyboard is lit while the screen is. A dark screen leaves the keyboard dark either way.

PROP 4879: PROP_KEYBOARD_AIDS

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_KEYBOARD
  • Value Type: UINT8
  • Post-Reset Value: 3 (every aid), or restored from saved state

The typing aids in effect, as a bitmap:

BitNameAid
0 (0x01)KEYBOARD_AID_CAPITALIZEThe first letter of a sentence is capitalized.
1 (0x02)KEYBOARD_AID_PERIODTwo spaces typed after a word become a period and a space.

The other bits are reserved and MUST be zero. A write that sets one is answered STATUS_INVALID_ARGUMENT.

All three properties are included in CMD_SAVE, and are asynchronous because the device’s own controls change them (see Changes Made at the Device).

Sound

A device with a sounder of its own exposes whether it plays optional sounds, and how loud, in the device domain, so the choice is saved with the rest of its configuration and reachable from wherever it is administered.

PROP 4891: PROP_SOUND_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_SOUND
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether the device plays its optional sounds: the notifications and feedback cues it makes of its own accord. Cleared, the device makes none of them.

It governs sound alone. Haptics and visual indication are unaffected, and a locate alert sounds whatever this property says.

PROP 4892: PROP_SOUND_VOLUME

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_SOUND
  • Value Type: UINT8
  • Post-Reset Value: device-defined, or restored from saved state

How loud the device plays its optional sounds. 255 is the loudest it plays them, and 0 the quietest level that is not silent: volume never silences the device, which is PROP_SOUND_ENABLED’s to do.

The levels in between are the device’s to map onto its sounder, and it reports the value written. A locate alert is not governed by this property: it sounds as loud as the device makes it.

Both properties are included in CMD_SAVE, and are asynchronous because the device’s own controls change them (see Changes Made at the Device).

Setup

A device with a messaging interface of its own says whether it has been set up through that interface, so a host can tell a device new from the box, or from a factory reset, from one already in use.

PROP 4893: PROP_SETUP_COMPLETE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_KEYBOARD
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether the device has been set up through its own interface. The device sets it when its first-run setup finishes. While it is clear, unlocking the device’s interface opens that setup, so a host that clears it sends whoever holds the device through setup again, and one that sets it skips setup.

The property is included in CMD_SAVE, so CMD_FACTORY_RESET clears it with the rest of the saved state. It is asynchronous because the device sets it itself (see Changes Made at the Device).

PROP 4894: PROP_TEST_MESSAGE_ALERTS

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether test messages on EMERGENCY call for attention like any other message. Cleared, a test message earns no receipt cue, and a device with an interface of its own neither alerts for it nor counts it unread. The message is still queued, stored, and shown.

The property is included in CMD_SAVE.

ULCP: Saved State

A radio that only works while a phone is talking to it is a peripheral. A radio that comes back after a power cut still configured, still forwarding, with nobody present, is infrastructure. Saved state is the difference: a device snapshots its device-domain configuration to non-volatile storage and restores it at boot before anything else happens.

Capabilities

CodeNameGrants
36CAP_SAVECMD_SAVE, CMD_RESTORE, PROP_SAVED, and boot-time restoration of saved state

CMD_CLEAR is available regardless of capabilities: a device with nothing persisted succeeds trivially.

Saved State

A device advertising CAP_SAVE can snapshot its provisioning to non-volatile storage so that it can operate autonomously across power cycles—the radio can be powered on in the morning with no phone present, restore its configuration, enable the PHY, and resume operating—forwarding, if it is a repeater.

  • CMD_SAVE atomically writes the current device domain configuration—including the RF configuration and the current value of PROP_PHY_ENABLED—to non-volatile storage, replacing any previous snapshot.

    The host domain is never part of a snapshot (see Host Domain): a radio’s autonomy is its own configuration, and whichever host it is serving re-establishes its keys, filters and delegation policy on every attach. Dynamic read-only state, including queue contents and PROP_BATTERY, is likewise never saved. The device identity keypair is excluded for a different reason: it is independently persisted the moment it is installed or generated (see PROP_DEV_PRIVATE_KEY) and is changed only by explicit provisioning or CMD_CLEAR—neither CMD_RESTORE nor a reboot can revert the device identity to an earlier key. Device lists a device’s own store keeps are excluded on the same grounds (see Lists Held by the Device’s Store).

    Traffic statistics (PROP_STAT_*) are also excluded. They are live hardware history since boot or the last explicit counter reset, not configuration a snapshot can restore.

    PROP_TIME is excluded for a third reason: a stored epoch is wrong by however long the device was off, by an amount nothing can bound, so restoring one would be restoring a confidently incorrect clock. A device recovers the time from a real source or reports that it does not know it. Its time zone and how it writes hours are ordinary configuration and are saved.

    What a network link is currently doing is excluded on the statistics’ reasoning. PROP_WIFI_NETWORK, PROP_WIFI_SCANNING, PROP_WIFI_SCAN_RESULTS, PROP_WIFI_LINK, PROP_WIFI_RSSI, PROP_WIFI_AP_STATE, PROP_WIFI_AP_CLIENTS, and every IP property but the two configurations and PROP_IP_DNS describe what the device found in front of it, not what its operator asked for. A restored lease or a restored association would be a claim about a network the device may not be near. The Wi-Fi configuration that produces them, credentials included, is saved, which is what lets a device rejoin unattended.

    The bridge client enable flag, host, port, and server identity pin are saved as device configuration. Its live link state and tunnel queues are excluded. The client authenticates using the independently persisted device identity; saving bridge configuration does not copy its private key into the snapshot.

  • At boot, if a snapshot exists, the device MUST restore it and resume operation accordingly before processing any host command: the RF configuration is applied and the PHY is re-enabled if it was enabled when saved, so a repeater is forwarding before anything else happens. Host-domain behavior—filtering, queueing, acknowledgement delegation— does not resume, because there is no host domain until a host provides one. If no snapshot exists, all properties take their documented post-reset values.

  • CMD_RESTORE reverts the device domain to the snapshot on demand, letting the host abort uncommitted configuration changes—without rebooting the hardware or dropping the ULCP link. It is observable either as a protocol reset (STATUS_RESET_RESTORED) or as a series of property-update publications; hosts handle both.

  • CMD_CLEAR erases the snapshot and all other persisted provisioning, including the device identity private key and any device lists the device’s own store keeps. It does not modify live (in-RAM) state; a subsequent CMD_RST completes a factory reset. Transport-level state such as BLE bonds is not affected.

  • PROP_SAVED reports the state of the stored snapshot, which is not simply whether one exists—see Snapshot Integrity.

Saving is explicit rather than automatic: nothing a host or an administrator changes is written to non-volatile storage until it asks (the device identity, PROP_BLE_PAIRING_PIN, and device lists a device’s own store keeps are persisted as they are set). This gives the host control over flash wear and a well-defined “known good” configuration, and it means a radio never persists provisioning its host did not deliberately ask to keep. A change made at the device follows its own rules (see Changes Made at the Device).

Two consequences deserve emphasis:

  • Post-reset values come from the snapshot. CMD_RST reverts properties to their post-reset values, as always—but on a device with a snapshot, the post-reset value of every saved property is its saved value, not its documented default. This applies to the device domain only; the host domain has no saved value and always returns to its documented defaults. Factory defaults are restored by CMD_CLEAR followed by CMD_RST. A host that expects documented defaults after CMD_RST will find the PHY already configured and enabled on a radio that was provisioned for autonomous operation; such a host still works if it explicitly sets the properties it cares about.
  • Queue contents and replay baselines are not saved. Frames queued before a power loss are gone afterward, even if they were acknowledged on the host’s behalf—the sender believes them delivered. Likewise the per-peer frame-counter baselines used by acknowledgement delegation restart (see Counter Resynchronization). These share the host domain’s lifetime, which is why re-provisioning after a power cycle is a resynchronization point rather than an inconvenience. Implementations MAY persist the queue to narrow this window, but hosts MUST NOT rely on it.

Snapshot Integrity

The snapshot is the one piece of state whose loss is silent and remote. A device configured to operate unattended comes back from a rejected snapshot deaf and non-forwarding, with nobody attached to be told, and recovery requires physically visiting it. The requirements below exist for that case.

  • A snapshot MUST be self-describing enough that a device can distinguish a payload it cannot read from an absent one. A device MUST NOT apply a payload it does not fully understand.
  • Devices MUST NOT silently boot bare after rejecting a snapshot. Where the storage retains earlier generations, the device MUST fall back to the newest generation that does decode, in preference to booting with documented defaults. A device MAY bound how far back it walks.
  • PROP_SAVED MUST report a fallback and an unreadable snapshot distinguishably from both “saved” and “nothing saved” (see PROP_SAVED). Devices with a local indicator SHOULD signal it there as well, since the host-visible report reaches nobody on an unattended device.
  • A device that restored an older generation is otherwise in normal operation: nothing is refused, and CMD_SAVE replaces the stored snapshot and clears the condition.

Only forward compatibility is required. Newer firmware MUST read snapshots written by older firmware, taking the documented default for anything the older writer did not record, and MUST ignore content it does not recognize. Firmware downgrade is out of scope: an older image reading a newer snapshot has no defined behavior, and saving from a downgraded image is destructive by design.

Changes Made at the Device

A device’s own controls—a switch, a menu, or a settings interface of its own (see The Device’s Own Interface)— change saved properties with no host asking. Such a change:

  • updates an existing snapshot that is in effect (PROP_SAVED reports 1), so what was set at the device is still set after a reboot;
  • never creates one. A device with nothing saved saves nothing: a snapshot made from a button press would persist every other live value with it. A device that rejected its newest generation at boot (PROP_SAVED reports 2 or 3) writes nothing either, since the write would bury the condition PROP_SAVED reports.

The change applies live in every case.

A device with a full settings interface MAY save as a host would: a snapshot at first boot, so that one always exists, and CMD_SAVE after a change whose loss would matter, such as its name or radio configuration. Either save captures the whole device domain, including any change an attached host or administrator made and did not save.

Commands

IdMnemonicDirDescription
12CMD_SAVEHost->DeviceSave state to non-volatile storage
13CMD_CLEARHost->DeviceErase all saved state
14CMD_RESTOREHost->DeviceRestore state from the saved snapshot
15CMD_FACTORY_RESETHost->DeviceErase all mutable state (incl. bonds) and reboot

CMD 12: (Host -> Device) CMD_SAVE

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_SAVE   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_SAVE

Save state. Commands the device to atomically write the current device-domain configuration to non-volatile storage as described in Saved State, replacing any existing snapshot. The command payload SHOULD be empty and MUST be ignored.

The response is a CMD_PROP_IS for PROP_LAST_STATUS with the command’s TID: STATUS_OK once the snapshot is durably stored, or an appropriate error status (for example STATUS_NOMEM) if it is not; on failure the previous snapshot, if any, MUST remain intact.

This command is only available on devices advertising CAP_SAVE; otherwise it fails with STATUS_UNIMPLEMENTED.

CMD 13: (Host -> Device) CMD_CLEAR

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |   CMD_CLEAR   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_CLEAR

Clear saved state. Commands the device to erase from non-volatile storage the saved snapshot and all other persisted provisioning, including the device identity private key and any device lists its own store keeps (see Lists Held by the Device’s Store). Live (in-RAM) state is unaffected; transport-level state such as BLE bonds and PROP_BLE_PAIRING_PIN is also unaffected, as are the conversations of a device’s own messaging interface. A CMD_CLEAR followed by CMD_RST restores factory protocol behavior.

Because a device identity always exists (see The Device Identity), the CMD_RST that completes the sequence MUST generate and persist a new one rather than leave the device with none—the same thing a factory-fresh power-on does, and for the same reason. PROP_DEV_KEY therefore reports a different key after the sequence, never an empty one.

The previous identity is gone from the moment CMD_RST completes, but anything the device built around it—a running device node, in particular— MUST NOT continue to originate traffic under it, even where that state survives until the next boot.

The command payload SHOULD be empty and MUST be ignored. The response is a CMD_PROP_IS for PROP_LAST_STATUS with the command’s TID.

Unlike CMD_SAVE, this command is available regardless of capabilities; a device with nothing persisted succeeds trivially.

CMD 14: (Host -> Device) CMD_RESTORE

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |  CMD_RESTORE  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_RESTORE

Restore saved state. Commands the device to revert its device-domain configuration to the contents of the saved snapshot (see Saved State). Regardless of how completion is reported (below), the resulting state is the same:

  • saved device-domain properties take their saved values, and the saved RF configuration and PHY enable state are applied;
  • the hardware is not reset, and the transport link and attach state are preserved;
  • the host domain is not touched: it is not part of a snapshot, so a restore has nothing to revert it to. The inbound queue contents, per-peer replay baselines, filters and delegation policy all survive unconditionally;
  • independently persisted state outside the snapshot—the device identity keypair, PROP_BLE_PAIRING_PIN, and device lists the device’s own store keeps—is not affected; and
  • the saved snapshot itself is not modified.

A restore never enables the PHY under an identity the snapshot was not taken for. A snapshot records which device identity was live when it was written. If that does not match the live PROP_DEV_KEY, the device MUST apply the restore with PROP_PHY_ENABLED false, whatever the snapshot says.

This is the replacement-hardware case, and it is the one path where a freshly generated identity can reach the air. Restoring a repeater’s saved domain onto a new board before installing that repeater’s key (see PROP_DEV_PRIVATE_KEY) would otherwise bring the radio up advertising as the node the snapshot describes, signing as a key nobody has ever seen. Installing the key first, then restoring, is the intended order and enables the PHY normally; the rule is what makes the wrong order safe rather than merely discouraged. A snapshot that does not record an identity is treated as matching.

Together with CMD_SAVE, this provides a commit/abort pattern: the host can make live configuration changes and either persist them with CMD_SAVE or discard them with CMD_RESTORE.

The command payload SHOULD be empty and SHOULD NOT be processed. A device reports a successful restore in one of two forms, both valid; the two forms differ only in reporting and in session-state handling, never in the resulting configuration or retained data:

  • Reset form—the device additionally resets its protocol session state (transaction bookkeeping and session-scoped properties), as on attach. As with CMD_RST, the TID is ignored; completion is signaled by an unsolicited CMD_PROP_IS for PROP_LAST_STATUS carrying the reset code STATUS_RESET_RESTORED (see Reset Codes). On receiving it, the host discards its cached view of all properties and assumes saved properties hold their saved values; dynamic read-only properties (such as PROP_HOST_RX_QUEUE_COUNT) reflect live state and are re-fetched.

  • Update form—the device applies the revert in place, emitting an unsolicited CMD_PROP_IS (with key material omitted, where applicable) for every property whose value changed, and then reports completion with CMD_PROP_IS for PROP_LAST_STATUS carrying STATUS_OK and the command’s TID. Session state is not reset in this form.

A host MUST handle both forms: it treats STATUS_RESET_RESTORED as full reversion to saved values, applies any unsolicited property updates, and recognizes completion by either the reset notification or the matching-TID STATUS_OK. This is not an extra burden in practice—hosts must already tolerate unsolicited CMD_PROP_IS value changes at any time (see Attach, Detach, and Synchronization). A host that does not know the snapshot’s contents (for example, because a previous session saved it) re-fetches the properties it depends on, exactly as in the post-attach procedure.

If an error occurs—in particular STATUS_INVALID_STATE when no snapshot exists (see PROP_SAVED)—the value of the emitted PROP_LAST_STATUS will be set accordingly, no state is modified, and no reset code is emitted.

CMD 15: (Host -> Device) CMD_FACTORY_RESET

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |CMD_FACTORY_RST|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_FACTORY_RESET

Return the radio to a blank factory state. Commands the device to erase every piece of mutable state it holds—both the persisted state CMD_CLEAR erases (the saved snapshot, all persisted provisioning, and the device identity private key) and the state CMD_CLEAR deliberately preserves: all BLE bonds, the configured PROP_BLE_PAIRING_PIN, and the conversations of a device’s own messaging interface—and then reboot. After the reboot the radio is indistinguishable from one that has never been provisioned or paired.

This differs from CMD_CLEAR + CMD_RST in two ways: it also clears transport-level pairing state (bonds and PIN) and conversations, and it performs a hardware reboot rather than only a protocol-session reset.

The command payload SHOULD be empty and MUST be ignored. Unlike every other command, CMD_FACTORY_RESET has no response: the device wipes its storage and reboots, which drops the transport link. A host treats the ensuing disconnect (and the radio’s subsequent reappearance in a factory state) as completion; it MUST NOT wait for a PROP_LAST_STATUS. The TID is therefore irrelevant.

Because clearing the bonds invalidates the encrypted link the command arrived on, a host that issues CMD_FACTORY_RESET over a bonded transport should also discard its own pairing to the radio.

This command is only available on devices advertising CAP_REBOOT, since it ends in one. A device without it answers STATUS_UNIMPLEMENTED—the only response this command ever produces.

Properties

IdMnemonicCommandsDescription
49PROP_SAVEDGetSaved-snapshot state

PROP 49: PROP_SAVED

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_SAVE
  • Value Type: UINT8

Whether a saved snapshot is in effect (see Saved State)—that is, whether the device is armed for autonomous operation across a power cycle— and, when the answer is qualified, how:

ValueMeaning
0Nothing is saved. Every property holds its documented default.
1The most recently saved snapshot is in effect.
2A saved snapshot is in effect, but a newer stored generation was rejected at boot and this is an earlier one. The device is operating on configuration older than what was last saved.
3A snapshot exists but no stored generation could be read. The device booted with documented defaults despite having been saved.

Values 2 and 3 are conditions to report to the operator, not errors to recover from automatically: the configuration the device is running is not the configuration that was last written, and only whoever wrote it can say what should replace it. A successful CMD_SAVE returns the value to

  1. Values 2 and 3 persist for the remainder of the power cycle and MUST NOT be cleared by CMD_RST or CMD_RESTORE, neither of which re-reads storage.

A host that treats any non-zero value as “saved” behaves correctly, and loses only the warning.

ULCP: Tethered Host Services

A device is most useful to a phone when it can keep working while the phone is asleep or out of range. The services in this chapter are what that means concretely: the device learns which traffic is relevant to its host, holds that traffic while the host is away, and—for peers the host has explicitly provisioned—acknowledges it so that senders’ retransmission logic is satisfied.

Everything here is assistance, tightly scoped. The host still owns the UMSH MAC and its own private keys; the device is helping its host, not impersonating it in the general case.

The Tethered Host Identity

The tethered host identity is the single UMSH identity owned by the attached host. Of the identity keypair itself, the device holds only the 32-byte public key; the host’s private key MUST NOT be transferred to the device, and this protocol provides no mechanism for doing so (see Security Boundary). The device may additionally hold host-domain state derived or delegated by the host— channel keys, per-peer symmetric keys, filters, and queued traffic—as defined in this chapter.

Because the device never holds the host’s private key, it cannot perform ECDH on the host’s behalf. All pairwise key material the device uses for the host identity is derived by the host and explicitly provisioned per peer (see PROP_HOST_PEER_KEYS).

ULCP supports exactly one tethered host identity at a time. Everything provisioned for it forms the host domain, which is volatile across a power cycle and wiped wholesale when a different host identity takes over the device.

Host Replacement

The host domain is keyed by PROP_HOST_KEY. Setting PROP_HOST_KEY to a value different from its current value—including setting it to empty —MUST atomically reset the entire host domain to defaults: the key tables, filter table, and mute tables are cleared, PROP_HOST_AUTO_ACK reverts to false, and the inbound queue is discarded. Because the host domain is never persisted, this is a live-state operation with no durable component: a power cycle cannot resurrect a previous host’s provisioning regardless.

Setting PROP_HOST_KEY to its current value is idempotent and has no side effects.

This rule is what makes re-pairing safe: when a companion radio is paired with a different phone, the new host configures its own identity and the previous host’s keys, filters, and queued traffic cease to exist—while the device domain (the radio’s own identity, channels, and settings) is untouched.

Capabilities

CodeNameRequiresGrants
32CAP_HOST_FILTER—PROP_HOST_KEY, PROP_MAC_PROMISCUOUS, PROP_HOST_RX_FILTERS, and the receive-filtering behavior
33CAP_HOST_RX_QUEUECAP_HOST_FILTERThe inbound queue, its properties, CMD_QUEUE_DRAIN, the buffered-frame metadata, and the mute tables
34CAP_HOST_KEYSCAP_HOST_FILTERPROP_HOST_CHANNEL_KEYS and PROP_HOST_PEER_KEYS
35CAP_HOST_AUTO_ACKCAP_HOST_KEYS, CAP_HOST_RX_QUEUEPROP_HOST_AUTO_ACK and acknowledgement delegation
48CAP_MAC_BACKHAULCAP_DEV_IDENTITYPROP_MAC_BACKHAUL and the point-to-point link to the device’s node

A device advertising none of these is a transparent radio: it delivers every frame it receives and holds nothing on anyone’s behalf.

Commands

IdMnemonicDirDescription
11CMD_QUEUE_DRAINHost->DeviceDeliver queued inbound frames

CMD 11: (Host -> Device) CMD_QUEUE_DRAIN

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |CMD_QUEUE_DRAIN|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_QUEUE_DRAIN

Deliver queued inbound frames. Commands the device to deliver every frame currently held in the inbound queue (see Inbound Queueing), oldest first, as ordinary CMD_STR_RECV commands on STR_PHY_RAW carrying the buffered-frame metadata described in Buffered-Frame Metadata. The command payload SHOULD be empty and MUST be ignored.

Queued frames are only delivered in response to this command; attaching to the device does not by itself cause queued frames to be delivered (see Inbound Queueing). This lets the host finish synchronizing its session and signal that it is actually ready to process backlogged traffic.

The drain covers exactly the frames held in the queue when the command is received. Because accepted frames are always delivered live while a host is attached, the queue cannot grow while a drain is in progress: the drain always covers a fixed set of frames and always terminates. If the command was sent with a non-zero TID, the device reports completion by emitting CMD_PROP_IS for PROP_LAST_STATUS with STATUS_OK and the matching TID immediately after delivering the last covered frame. Draining an empty queue succeeds immediately.

Frames that arrive while a drain is in progress are not part of it: they are delivered live, and MAY therefore interleave with the buffered deliveries. RX_FLAG_BUFFERED distinguishes the two, and UMSH does not guarantee in-order delivery in any case (see Inbound Queueing).

If the device does not implement queueing (CAP_HOST_RX_QUEUE not advertised), the command fails with STATUS_UNIMPLEMENTED.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

Properties

The host domain occupies property identifiers 96–111. Two properties sit outside that range: PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL govern how the attached session sees the radio rather than how the device is provisioned, and are the protocol’s session-scoped properties.

IdMnemonicCommandsDescription
48PROP_MAC_PROMISCUOUSGet, SetDeliver all frames (session-scoped)
50PROP_MAC_BACKHAULGet, SetPoint-to-point link to the device’s node (session-scoped)
96PROP_HOST_KEYGet, SetTethered host identity public key
97PROP_HOST_CHANNEL_KEYSGet, Set, Insert, RemoveHost channel keys
98PROP_HOST_PEER_KEYSGet, Set, Insert, RemoveHost pairwise peer keys
99PROP_HOST_RX_FILTERSGet, Set, Insert, RemoveHost receive filter table
100PROP_HOST_AUTO_ACKGet, SetAcknowledgement delegation enable
101PROP_HOST_RX_QUEUE_COUNTGetFrames currently queued
102PROP_HOST_RX_QUEUE_CAPACITYGet, SetQueue capacity in frames
103PROP_HOST_RX_QUEUE_DROPPEDGetFrames dropped from the queue
104PROP_HOST_MUTED_CHANNELSGet, Set, Insert, RemoveChannels whose queued frames raise no cue
105PROP_HOST_MUTED_PEERSGet, Set, Insert, RemovePeers whose queued frames raise no cue

PROP 48: PROP_MAC_PROMISCUOUS

  • Type: Single-Value, Read-Write, Session-Scoped
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Value Type: BOOL
  • Post-Attach Value: 0 (false)

When true, every frame the PHY successfully receives is delivered to the host over STR_PHY_RAW, bypassing receive filtering. This is a live-session diagnostic mode: frames that are delivered only because of promiscuous mode are never queued while the host is detached, and never acknowledged on the host’s behalf.

Session-scoped: it reverts to false on every attach.

PROP 50: PROP_MAC_BACKHAUL

  • Type: Single-Value, Read-Write, Session-Scoped
  • Asynchronous Updates: No
  • Required: CAP_MAC_BACKHAUL
  • Value Type: BOOL
  • Post-Attach Value: 0 (false)

When false, the host and the device’s own node are two listeners on one shared medium. Both transmit through the same radio and both hear what it receives, so a frame from one reaches the other only by way of some third node that repeats it.

When true, the host is instead a point-to-point neighbor of the device’s node:

  • A frame the host sends on STR_PHY_RAW is delivered to the node as though the node had heard it, and is never transmitted directly. It spends no airtime and is not subject to the duty-cycle limit.
  • Frames the node transmits are delivered to the host with RX_FLAG_SELF_TX set, subject to the usual receive filtering.
  • Frames the radio receives are not delivered to the host at all. The node is the only thing listening to the medium.

Traffic between the host and the mesh therefore crosses the device’s repeater, which is what makes the arrangement useful to an internet bridge: hop accounting, duplicate suppression, and forwarding policy are the node’s, applied to the host’s traffic as to anyone else’s. A device whose repeater is disabled still delivers the host’s frames to its own identities, but carries nothing onward.

PROP_MAC_PROMISCUOUS composes with this: it removes receive filtering from what the host is delivered, which in this mode is the node’s transmissions.

Session-scoped: it reverts to false on every attach.

PROP 96: PROP_HOST_KEY

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Empty

The Ed25519 public key of the tethered host identity. Setting this property tells the device which node identity it is assisting; an empty value means no host identity is configured.

Setting this property to a value different from its current value resets the entire host domain, as specified in Host Replacement. Setting it to its current value is idempotent.

Like the rest of the host domain, this property is never saved: it is empty at every power-on, whatever the radio was doing before.

A configured host key acts as an implicit destination-hint receive filter (see Receive Filtering).

PROP 97: PROP_HOST_CHANNEL_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_KEYS
  • Item Form: 32 octets (the channel key)
  • Reported Form: 16 octets (the derived channel identifier)
  • Remove Selector: the 32-octet channel key
  • Post-Reset Value: Empty

The set of channel keys provisioned for the host identity. For each key the device derives the channel identifier and the channel K_enc/K_mic. Each entry is reported as its full 16-octet channel identifier, which is the width at which two channels can be told apart; the key itself is never read back.

The identifier’s leading 2 octets are the channel_id the CHANNEL field carries, and they act as an implicit channel receive filter (see Receive Filtering). Host channel keys serve two assistance purposes:

  • recognizing multicast traffic on the host’s channels while the host is detached, so it can be queued; and
  • recognizing blind unicast traffic addressed to the host identity, which requires the channel key to decrypt the concealed destination/source addresses (see Blind Unicast Processing) and to form the combined blind unicast payload keys used for authentication and acknowledgement.

Channel keys are group-membership credentials, not host private keys, so provisioning them is consistent with the security boundary. They still grant whoever holds the device the ability to read and send traffic on those channels; see Provisioning Security.

PROP 98: PROP_HOST_PEER_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_KEYS
  • Item Form: Structure, 96 octets
  • Reported Form: 32 octets (the peer’s public key)
  • Remove Selector: the 32-octet peer public key
  • Post-Reset Value: Empty

Pairwise symmetric key material provisioned for specific already-known peers of the host identity. The item form is:

+---------------------+-----------+-----------+
| PEER_PUBLIC_KEY     |   K_ENC   |   K_MIC   |
+---------------------+-----------+-----------+
        32 B              32 B        32 B

Figure: Peer key entry item form

Where PEER_PUBLIC_KEY is the peer’s Ed25519 public key and K_ENC and K_MIC are the stable pairwise keys for the (host, peer) pair, derived by the host as described in HKDF Inputs for Unicast. The device never derives these itself—it cannot, because it does not hold the host’s private key.

As an exception to the usual CMD_PROP_INSERT duplicate rule, inserting an entry whose PEER_PUBLIC_KEY matches an existing entry replaces that entry. Replacement updates only the stored key material: the peer’s replay baseline (see Acknowledgement Delegation) and any frames already queued from that peer are unaffected, since both are keyed by the peer’s identity rather than by the key values. An entry is reported as its peer public key alone: K_ENC and K_MIC are never read back.

Provisioned peer keys let the device authenticate inbound unicast and blind unicast from those specific peers and acknowledge it on the host’s behalf (see Acknowledgement Delegation). They grant no capability regarding any other peer, and do not allow the device to establish new pairwise relationships.

PROP 99: PROP_HOST_RX_FILTERS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Item Form: Structure
  • Remove Selector: the full item
  • Post-Reset Value: Empty

The explicit receive filter table. Each item is a filter entry:

+-------------+----------------------+
| FILTER_TYPE | FILTER_VALUE ...
+-------------+----------------------+
     1 B          type-specific

Figure: Filter entry format

TypeNameValueMatches
0FILTER_DEST_HINT3 octetsFrames whose destination hint field equals the value
1FILTER_CHANNEL_ID2 octetsChannel-addressed frames (MCST, BUNI, BUAR) whose channel identifier equals the value
2FILTER_PKT_TYPE1 octetFrames whose FCF packet-type field equals the value (0–7)

Entries with an unrecognized FILTER_TYPE, or whose value length does not match the type, fail with STATUS_INVALID_ARGUMENT.

See Receive Filtering for how this table combines with the implicit filters derived from PROP_HOST_KEY and PROP_HOST_CHANNEL_KEYS.

PROP 100: PROP_HOST_AUTO_ACK

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_AUTO_ACK
  • Value Type: BOOL
  • Post-Reset Value: 0 (false)

When true, the device sends MAC acknowledgements on behalf of the host identity for qualifying frames received while the host is detached, as specified in Acknowledgement Delegation. When false, the device never transmits on the host identity’s behalf.

PROP 101: PROP_HOST_RX_QUEUE_COUNT

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Value Type: UINT16_LE
  • Units: frames
  • Post-Reset Value: 0

The number of frames currently held in the inbound queue. The host typically reads this right after attaching to decide whether (and when) to issue CMD_QUEUE_DRAIN.

PROP 102: PROP_HOST_RX_QUEUE_CAPACITY

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE (CMD_PROP_SET support is OPTIONAL)
  • Value Type: UINT16_LE
  • Units: frames
  • Post-Reset Value: Implementation-Specific

The maximum number of frames the inbound queue can hold. Devices with a fixed queue size fail CMD_PROP_SET with STATUS_UNIMPLEMENTED; devices that allow adjustment fail values they cannot honor with STATUS_INVALID_ARGUMENT.

PROP 103: PROP_HOST_RX_QUEUE_DROPPED

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Value Type: UINT32_LE
  • Units: frames
  • Post-Reset Value: 0

The cumulative number of frames discarded from the inbound queue—evicted by the circular queue-full policy or otherwise not retained (see Inbound Queueing)—since the device last reset. A non-zero increase across a detached interval tells the host that its view of that interval is incomplete. The counter wraps modulo 2^32.

PROP 104: PROP_HOST_MUTED_CHANNELS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Item Form: 16 octets (the full channel identifier)
  • Remove Selector: the item
  • Post-Reset Value: Empty

The channels whose queued messages raise no receipt cue. A frame matches when the provisioned channel key that authenticated it derives a listed identifier; the match is made after verification, against the full identifier, never against the 2-octet on-wire one, so an identifier collision cannot mute a channel the host did not name.

PROP 105: PROP_HOST_MUTED_PEERS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Item Form: 32 octets (a peer public key)
  • Remove Selector: the item
  • Post-Reset Value: Empty

The peers whose direct messages, once queued, raise no receipt cue. A frame matches when its resolved sender is a listed key; a peer’s posts to a channel are governed by PROP_HOST_MUTED_CHANNELS alone. Items are full public keys rather than 3-byte hints: hints collide, and the detached receive path resolves the full key already.

Receipt Cues

A device may indicate locally that it took a frame in for a host that is not attached—a sound, a light, a count on a panel. What form the indication takes, and whether a device makes one at all, is a property of the board rather than of the protocol.

An indication of one arrival is reserved for a message the operator reads: a frame the device authenticated with a provisioned key whose payload is a text message of type basic or status, counted once, at its first fragment. Resend requests, Message Unavailable responses, continuation fragments, a message whose Message Type, Regarding, or Editing option does not decode, and every other payload type are queued without one, as is any frame the device could not authenticate, since its content is unreadable, and any frame carrying a delivery-critical option the device does not recognize. A message on the EMERGENCY channel by multicast gets one only when its frame signature verifies against a key the device holds.

PROP_HOST_MUTED_CHANNELS and PROP_HOST_MUTED_PEERS name conversations whose messages are taken in without one: a channel, matched by the key that authenticated the frame, or a direct conversation, matched by its sender. They govern the indication and nothing else: a frame in a muted conversation is filtered, queued, acknowledged, and drained exactly as any other, and counts toward PROP_HOST_RX_QUEUE_COUNT and PROP_HOST_RX_QUEUE_DROPPED the same way.

A test message on EMERGENCY raises no indication of its arrival, and lights nothing that asks for attention, unless PROP_TEST_MESSAGE_ALERTS is set. Like a muted one, it is otherwise queued and drained as any other frame.

Indications that describe the queue as a whole rather than one arrival—a queued count, a “something is waiting” light—follow every other queued frame, because they describe what a drain will deliver.

The tables are not validated against the key tables. An entry naming a conversation the device holds no key for is simply inert, as an entry in PROP_HOST_RX_FILTERS is independent of the key tables.

Receive Filtering

Receive filtering determines which successfully received frames are accepted for the host—delivered live when the host is attached, or queued when it is not.

The device evaluates each received frame against the union of:

  • the explicit filters in PROP_HOST_RX_FILTERS;
  • an implicit destination-hint filter for the first 3 bytes of PROP_HOST_KEY, when a host key is configured; and
  • an implicit channel filter for the derived channel identifier of each key in PROP_HOST_CHANNEL_KEYS.

A frame matching any filter is accepted. Hints and channel identifiers are prefilters, not proof (see Addressing); filtering by them can only over-accept, never mis-reject, and the host performs full cryptographic verification as usual.

The implicit destination-hint filter matches unicast traffic addressed to the host identity. Encrypted blind unicast addressed to the host is matched through its channel filter (its destination hint is concealed on the wire); the device MAY additionally use a provisioned channel key to decrypt the address block and narrow the match.

Two kinds of returning traffic identify themselves only by the trailer of a frame the host previously sent, so no destination or channel filter can address them: a MAC Ack carries no destination hint and its public ack_mic is the first 4 bytes of the acknowledged frame’s MIC, and a repeater’s onward copy of a host frame keeps the host’s trailer while any destination hint it carries names the remote peer. The device therefore records the leading 4 trailer bytes of each frame it transmits on the host’s behalf—for a MAC Ack, its ack_mic—and implicitly accepts any received frame whose trailer opens with a recorded value. For a MAC Ack the host sent, this matches the ack being carried onward; for any other frame, it matches both the returning acknowledgement and the frame being carried onward. The host’s forwarding-confirmation machinery must overhear every onward copy, acks included, to stop retransmitting. These records evict lazily, so multiple echoes of one send— acks arriving over different return routes, repeats from different repeaters—are all delivered. A MAC Ack whose ack_mic matches no recorded frame is still accepted if an explicit FILTER_PKT_TYPE entry selects it.

Broadcast packets—payload-carrying broadcasts and beacons alike—are implicitly accepted for live delivery: a broadcast is addressed to every node, the host included. The rule is live-only. While the host is detached, a broadcast is queued only when an explicit filter selects it (e.g., a FILTER_PKT_TYPE entry with value 0), so ambient broadcast traffic cannot displace queued unicast frames.

Directed traffic that asks for no acknowledgement follows the same shape. A UNIC or BUNI frame accepted only by an implicit filter is delivered live but MUST NOT be queued; queueing it requires an explicit FILTER_PKT_TYPE entry selecting that packet type. Its sender is not waiting on the device—nothing about it will be repeated or given up on— so the frame is a request whose asker has moved on by the time a drain runs, while the queue slot it takes is one an ack-requesting frame needed. The rule is stated as a default the host must override rather than one it can refuse, because filters only widen: an entry for UNAR would add to what the implicit destination-hint filter already admits, never narrow it.

Device-domain state never creates implicit host filters: frames for the device identity or its channels reach the host only if the host’s own filtering matches them.

Compatibility rule: when no host key is configured, no host channel keys are provisioned, and the explicit filter table is empty, filtering is considered unconfigured and every successfully received frame is accepted for live delivery. This is exactly how a device with no host services at all behaves, so a host using the radio as a plain frame pipe observes no difference on a filtering device in its factory state. As soon as any filter (implicit or explicit) exists, only matching frames are accepted.

Like the broadcast rule, this one is live-only. A device with filtering unconfigured queues nothing while detached: the host domain does not survive a power cycle, so every device passes through this state on the way from power-on to its host’s first write, and a device that queued here would fill its queue with whatever was in earshot—its own transmissions included—on nobody’s behalf.

A device MUST NOT queue a frame it transmitted itself. The copy a device delivers of its own transmission (RX_FLAG_SELF_TX) lets an attached host’s MAC observe its frame reaching the air; replayed from the queue it describes a transmission the host completed long before.

Promiscuous mode (see PROP_MAC_PROMISCUOUS) bypasses filtering for live delivery only.

Inbound Queueing

When CAP_HOST_RX_QUEUE is supported and the host is detached, accepted frames are placed in a FIFO inbound queue instead of being discarded. Each queue entry records the frame, its receive metadata (RSSI, LQI, SNR), the time of reception, and whether the device acknowledged it (see Acknowledgement Delegation).

When the host is attached, accepted frames are delivered live over STR_PHY_RAW as they always are. Attaching does not flush the queue: frames queued while the host was away remain queued until the host issues CMD_QUEUE_DRAIN. Frames received after attach are therefore delivered live even while older frames remain queued, and live deliveries MAY interleave with buffered deliveries during a drain (RX_FLAG_BUFFERED distinguishes them). A host that wants to process the backlog first drains promptly after attaching and MAY defer its processing of interleaved live deliveries; RX_AGE in the buffered-frame metadata gives coarse (one-second) relative timing but is not sufficient to reconstruct a strict total order—and UMSH itself does not guarantee in-order delivery in any case.

The queue is circular: when a new frame is accepted and the queue is full, the oldest queued frame is discarded and the new frame is appended. The queue therefore always holds the most recent accepted traffic. Every frame discarded by this eviction increments PROP_HOST_RX_QUEUE_DROPPED.

Eviction can discard a frame that was already acknowledged on the host’s behalf—the sender believes it delivered, but the host will never receive it. This is the same best-effort custody semantic that applies to power loss (see Acknowledgement Delegation and Saved State): a delegated ack asserts volatile custody, not guaranteed delivery.

Duplicate detection for queueing uses the standard final-destination mechanisms of replay detection: per-peer frame-counter state and the recent accepted-MIC cache used for the backward window, where the device holds the keys to apply them. A frame identified as a previously accepted frame MUST NOT consume an additional queue slot; it is coalesced with the existing entry. A Route Retry form of a queued frame is the same logical packet (same MIC and frame counter) and coalesces with it. Coalescing a duplicate is separate from acknowledging it—a coalesced duplicate may still have its ack retransmitted under the duplicate-acknowledgement window (see Acknowledgement Delegation). For frames the device cannot authenticate (no provisioned keys), no protocol-defined duplicate detection applies and each received frame occupies its own entry.

Acknowledgement Delegation

With PROP_HOST_AUTO_ACK enabled, the device acknowledges qualifying inbound frames so that senders’ retransmission logic is satisfied while the host is away. The device MUST transmit a MAC ack for a received frame if and only if all of the following hold:

  1. PROP_HOST_AUTO_ACK is true and no host is attached.
  2. The frame’s packet type requests acknowledgement: UNAR, or BUAR where the device also holds the frame’s channel key.
  3. The frame is addressed to the host identity: its (possibly decrypted) destination hint matches PROP_HOST_KEY, and its source resolves to an entry in PROP_HOST_PEER_KEYS—by full public key when the S flag is set, or by unique 3-byte prefix match otherwise.
  4. The frame authenticates: its MIC verifies under the pairwise K_MIC for UNAR, or under the combined blind unicast payload keys for BUAR.
  5. The frame carries no delivery-critical option the device does not recognize or cannot interpret.
  6. The frame is accepted as new by the replay-detection rules, applied per provisioned peer. The device advances a peer’s replay baseline only when it accepts a frame from that peer into the queue; a frame it fails to store leaves the baseline unchanged, so its retransmissions remain acceptable later.
  7. The frame was placed in the inbound queue (see Inbound Queueing). Because the queue is circular, placement normally succeeds by evicting the oldest entry when full; a frame that nevertheless cannot be stored (for example, one exceeding the device’s buffer) is not acknowledged, so the sender keeps retrying until the host returns.

A delegated ACK also requires that no unconsumed source-route hints remain. An early overheard copy may be queued without an ACK; the first eligible duplicate can then supply the ACK without another queue entry. RX_FLAG_ACKED requires successful radio transmission.

Delegated ACKs follow the return-path guidance, carry a trace when one is due, and preserve the request’s region codes on both flooded and source-routed returns.

Duplicates. An authenticated frame that replay detection identifies as a previously accepted frame—typically a retransmission whose original ack was lost—is not queued again, but the device MAY retransmit its acknowledgement under the core duplicate-acknowledgement window: only when the frame authenticates and its counter is no more than 8 behind the peer’s baseline, and without advancing or otherwise modifying the replay baseline. Re-acknowledging a duplicate is independent of queue coalescing (see Inbound Queueing) and does not mark anything newly accepted. Frames farther behind the baseline MUST NOT be acknowledged.

Reboot. Per-peer replay baselines are not saved (see Saved State). After a device reset, the first authenticated frame accepted from a provisioned peer re-establishes that peer’s baseline at face value, exactly as on first contact (see Counter Resynchronization). The consequence is that after a reboot, previously captured authenticated frames may be accepted, queued, acknowledged, and given a receipt cue if replayed in a counter sequence acceptable from the newly established baseline. The host MAC remains authoritative for duplicate suppression when the frames are eventually delivered, so this creates a limited availability and resource-consumption window (queue slots, delegated acks, and receipt cues), but it does not permit forgery or duplicate application delivery. Implementations concerned about this threat MAY persist a compact per-peer counter watermark (batched or range-reserved to limit flash wear), but hosts MUST NOT assume they do.

Custody. A delegated ack acknowledges volatile custody by default: the frame is held in RAM until drained, and the loss window on power failure is documented in Saved State. Implementations that persist the queue provide durable custody, but hosts and application protocols MUST NOT rely on it.

The acknowledgement is an ordinary MAC Ack packet: the ack MIC is the first 4 bytes of the acknowledged frame’s MIC and the 4-byte ack tag is computed as specified in Ack Tag Construction, using the provisioned pairwise keys (combined with the channel keys for BUAR). The ack carries no destination hint. It is routed from what the acknowledged frame itself teaches, the way a host MAC routes an ack from the route that frame just taught it (see Route Learning); the device holds no other routing state for the host’s peers:

  • A frame carrying a complete trace is acknowledged down its router hints as the ack’s source route; the trace is accumulated most-recent first, so it already reads in return order. An empty trace is a direct neighbor and gets a direct ack.
  • A frame carrying no complete trace but a flood hop count gets FHOPS_REM initialized from its FHOPS_ACC, with any region-code options replayed. An FHOPS_ACC of zero is a direct neighbor.
  • A frame carrying a source-route option and no complete trace—including an emptied option, which the last repeater keeps for provenance—spent flood hops only past the route’s end, so its FHOPS_ACC is not a distance. The ack floods at a default budget of 5 flood hops, or at one more than FHOPS_ACC if that is larger.
  • A frame with neither option and no flood hop count was heard off the sender’s own transmitter and is acknowledged directly.

Each way back carries the margin its last hop earns, judged from the acknowledged frame alone, since the device knows nothing else about the sender: the reception itself for a direct neighbor, the earliest trace record’s signal for a traced path, and no measurement for a flood distance. A re-ack of a duplicate (see Duplicates above) always carries the margin. An ack with no margin and no route is sent with no flood hop count and no trace, which could only arrive empty.

An ack a repeater may carry includes a trace when the frame arrived with FHOPS_ACC greater than zero or carried a trace of its own, so the sender learns the return path from the only frame an ack-only exchange gives it.

Delegated ack transmissions use the device’s normal transmit path and are subject to the configured duty-cycle limit; the device MUST NOT exceed the limit to send an ack. An ack that cannot be sent leaves the queued frame marked unacknowledged.

Frames that are accepted but fail any of conditions 2–6—no peer key, no channel key, authentication impossible to evaluate, an unrecognized delivery-critical option—are still queued (subject to filtering); they are simply not acknowledged. The host performs its own verification after draining and may ack late if the application finds that useful.

While a host is attached, the device never acks on its behalf: live-delivered frames are the host’s responsibility. Acks generated by the device identity for its own traffic are ordinary device-node behavior and are not governed by this section.

ULCP: Wi-Fi

Wi-Fi control is the subsystem the host uses to configure and observe a device’s 802.11 hardware. It covers three functions, each its own capability, because the hardware that has one does not always have the others:

  • Scanning, listening for access points and reporting what was heard. Every Wi-Fi receiver can do this, including ones that can do nothing else.
  • The station, joining a network the device is given and staying on it.
  • The access point, offering a network of the device’s own for other stations to join.

This chapter specifies the interface, not what the connection carries. Addressing is IP Connectivity; a bridge tunnel, a time source, or a binding a client could attach through is a capability of its own.

Everything here is device-domain state. The configuration properties, the two switches, the network table, and the access point’s network, are part of a saved snapshot and survive a change of host. The rest is live: which network the station is on, what a scan found, what the link is doing, who is on the access point. Live state is never saved, and CMD_RST reaches it only through the configuration it reverts, so each live property’s post-reset value is declared as whatever the fact is at the time.

A device with a station behaves as a phone does: whenever it is enabled, it joins a known network in range and stays on it, with no host present and none ever required. That is why nothing here is a command: a device on Wi-Fi is infrastructure, and infrastructure that needs a phone to get back on the network after a power cut is not. Choosing a Network says which network it joins.

Capabilities

CodeNameRequiresGrants
53CAP_WIFI_SCAN—A Wi-Fi receiver the device can scan with: PROP_WIFI_SCANNING, PROP_WIFI_SCAN_RESULTS
54CAP_WIFICAP_WIFI_SCANA Wi-Fi station the device can enable and join networks with: PROP_WIFI_ENABLED, PROP_WIFI_NETWORKS, PROP_WIFI_NETWORK, PROP_WIFI_LINK
57CAP_WIFI_APCAP_WIFI_SCANAn access point the device can bring up: PROP_WIFI_AP_ENABLED, PROP_WIFI_AP_CONFIG, PROP_WIFI_AP_STATE, PROP_WIFI_AP_CLIENTS

Three capabilities on one base, and everything else discovered by asking. The scan is the base because it is what every Wi-Fi radio can do: a station that can join can always scan, and so can a radio that can beacon, so both requiring CAP_WIFI_SCAN is a fact about hardware rather than a policy, and the rule that a device advertises what its capabilities require does the rest.

The station and the access point do not require each other. Nearly every chip does both, but they are different functions with different state, and a device that has one and not the other is describable. A device advertising any of the three MUST serve every property it grants.

The two remaining station properties, PROP_WIFI_RSSI and PROP_WIFI_MAC, report things a stack may not expose, and a device that only scans has neither. A device that cannot answer them answers STATUS_PROP_NOT_FOUND, in the same exchange the host was already making, for the reason the BLE binding gives: a refusal is a complete answer, and a second capability buys a host nothing it cannot learn in the reply it is already waiting for.

None of the three requires CAP_SAVE. Without it the configuration is volatile and the device knows no networks after a power cycle, which is a worse device but a conforming one.

Properties

Allocated from the extended device and transport configuration range: the station and its scan in a block of sixteen after the BLE transport’s, the access point in a block after IP Connectivity, so that neither the station nor the stack has to move to make room.

IdMnemonicCommandsDescription
4880PROP_WIFI_ENABLEDGet, Set, IsWhether the station is up
4881PROP_WIFI_NETWORKSGet, Set, Insert, RemoveKnown networks and their credentials
4882PROP_WIFI_NETWORKGet, Set, IsThe network the station is on or joining, or empty
4883PROP_WIFI_SCANNINGGet, Set, IsWhether a scan is in progress
4884PROP_WIFI_SCAN_RESULTSGet, Is, InsertedWhat the current or last scan has found
4885PROP_WIFI_LINKGet, IsLink state, failure reason, and association
4886PROP_WIFI_RSSIGetSignal of the current association
4887PROP_WIFI_MACGetThe station’s MAC address
4912PROP_WIFI_AP_ENABLEDGet, Set, IsWhether the access point is up
4913PROP_WIFI_AP_CONFIGGet, SetThe network the device offers
4914PROP_WIFI_AP_STATEGet, IsWhether it is beaconing, and where
4915PROP_WIFI_AP_CLIENTSGet, Is, Inserted, RemovedWho is on it

4888 through 4895 are reserved for the station, and 4916 through 4927 for the access point.

PROP 4880: PROP_WIFI_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether the station is up. Cleared, the device MUST drop any association, abandon any scan in progress, and put the station into the lowest power state the platform offers. This is PROP_GNSS_ENABLED’s promise rather than PROP_BLE_ENABLED’s: on a battery-powered node a Wi-Fi radio that is merely idle is still the largest load on the board, and a property that only stopped reporting would solve nothing.

Set again, the device comes back up and chooses a network to join. The known-network table is configuration and is not disturbed in either direction, so turning the station off and on is not a way to forget anything. It does end a pause.

Off by default, for the reason PROP_GNSS_ENABLED is: a device that has never been given a network has nothing to spend the power on. Saved state overrides the default, which is how a commissioned device comes up connected.

Asynchronous because a device with a screen MAY offer the switch on it, and a switch someone can flip is a value that moves without the host asking.

A write of 1 answers STATUS_INVALID_STATE on a platform that cannot run the station alongside another radio it currently has on, the device’s own access point included. The device MUST NOT take the other radio down to honor the write; the host turns that one off first. A platform that coexists, which is most of them, answers the write like any other.

PROP 4881: PROP_WIFI_NETWORKS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_WIFI
  • Item Form: structure below
  • Reported Form: the item with its credential omitted
  • Remove Selector: the SSID
  • Post-Reset Value: Empty, or restored from saved state

The networks the device knows. Each item:

+-------+----------+----------+----------+------------------+
| FLAGS | SECURITY | SSID_LEN |   SSID   |  CREDENTIAL ...  |
+-------+----------+----------+----------+------------------+
   1 B      1 B        1 B      1-32 B    security-defined

Figure: Known-network item format

FLAGS bit 0 set means the network hides its SSID and the device probes for it by name rather than waiting to hear it. Bit 1 set means the credential is a raw 32-octet pairwise master key rather than a passphrase, which is valid only for the modes that derive one; a host that holds the key need never hand the device the passphrase, and a device never has to run the derivation. Bit 2 set means the device joins the network only when told to, never on its own; clear, the network is one the device chooses from. Bits 3 through 7 are reserved and MUST be zero.

SECURITY is the minimum acceptable station security, using these mode codes:

ValueNameCredential
0WIFI_SEC_OPENnone
1WIFI_SEC_OWEnone
2WIFI_SEC_WPA2passphrase of 8-63 octets, or a key
3WIFI_SEC_WPA3password of 1-128 octets
4WIFI_SEC_WPApassphrase of 8-63 octets, or a key
5WIFI_SEC_WEPnone defined
6WIFI_SEC_WPA2_ENTnone defined
7WIFI_SEC_WPA3_ENTnone defined
8WIFI_SEC_WPA3_ENT_192none defined

Values 9 through 15 are reserved. Sixteen is the ceiling because PROP_WIFI_SCAN_RESULTS reports these as a 16-bit set.

WIFI_SEC_OPEN is no security at all. WIFI_SEC_OWE is Enhanced Open: encrypted against anyone listening, authenticated against nobody, with no credential, and the only credential-free mode permitted on 6 GHz. An OWE entry names the network as the operator sees it. Where that is a plain OWE network the device joins it; where it is the visible half of an OWE transition deployment, the device follows the transition element to the hidden companion BSS and joins that. PROP_WIFI_LINK then reports the companion’s BSSID and frequency, which is all it ever reports of any association, while PROP_WIFI_NETWORK goes on naming the visible network the operator selected. The entry never names the companion, because the operator never saw it.

WIFI_SEC_WPA2 is WPA2-Personal and covers the WPA/WPA2 mixed networks that negotiate CCMP; WIFI_SEC_WPA is the TKIP-only remainder, which a device MAY decline to join. WIFI_SEC_WPA3 is WPA3-Personal, SAE, whose password is not a WPA2 passphrase: SAE puts no bounds on it, so the 8 to 63 rule does not apply, and the table admits up to 128 octets, which is where the more generous stacks stop. A device whose stack holds a shorter limit refuses a longer password with STATUS_UNIMPLEMENTED, since the entry is well-formed and the device is what cannot hold it. A password shared with a transition network’s WPA2 side is 8 to 63 octets by that side’s rule, which is the host’s to know.

The remaining four exist so that scan results can say what they heard. WEP is not worth a credential form, and the three enterprise modes are a provisioning surface this chapter does not open; they are numbered so that opening it later renumbers nothing.

A passphrase or password is the UTF-8 encoding of what the operator typed, with no terminator and no U+0000, and its length bounds count octets. The device MUST derive keys from exactly those octets: the PBKDF2 of 802.11 Annex J for WIFI_SEC_WPA2 and WIFI_SEC_WPA, and SAE over the octets directly. An ASCII passphrase therefore yields what every router yields, and a non-ASCII one yields what a router that accepted UTF-8 yielded. In both cases the host’s job is to hand over the same octets the router’s operator entered, and the device’s is not to reinterpret them. A raw key, FLAGS bit 1, is exactly 32 octets and skips the derivation.

An insert is refused with STATUS_INVALID_ARGUMENT when it is not well-formed: an empty SSID, a mode with no credential form, a credential of the wrong length or kind for its mode, a reserved mode, a reserved flag bit set. The SSID is never empty because an empty PROP_WIFI_NETWORK means no network, so an entry with no name could be stored and never joined; a hidden network has a real name, and it is only the advertisement that is blank. An insert is refused with STATUS_UNIMPLEMENTED when it is well-formed and this device cannot do it: a WPA3 entry on a chip without SAE, or a WPA entry on a device that declines TKIP. The two are different answers because a host acts on them differently. The first is a bug in the host, and the second is a reason to pick the next mode the network offers.

The station chooses the strongest mutually supported authentication mode compatible with the credential and meeting SECURITY. The codes are not a numerical ranking. The permitted relationships are:

Stored minimumPermitted authentication
OpenOpen or OWE, without credentials
OWEOWE; never open
WPA, passphraseWPA, WPA2, or WPA3
WPA2, passphraseWPA2 or WPA3
WPA3, passwordWPA3; never WPA2
WPA, raw PMKWPA or WPA2; never SAE
WPA2, raw PMKWPA2; never SAE

A WPA2 entry therefore continues to work when its network upgrades to WPA3 with the same SSID and password. A raw PMK cannot make that upgrade: SAE needs the password, which cannot be recovered from the key. Modes outside the credential’s authentication family are not upgrades.

The device MUST NOT use a mode below the stored minimum. Using a stronger mode does not modify that minimum or its saved value. A host that requires WPA3 writes WPA3; a host that permits WPA2 with opportunistic WPA3 writes WPA2. Lowering the minimum is an explicit host configuration change. Scan results still name the modes advertised by each access point; access-point configuration names the mode the device offers.

Items are keyed by SSID: the table holds at most one entry per network, and an insert whose SSID matches an existing entry replaces it and reports the item as inserted. That is the path for a wrong passphrase, a changed one, or a mode upgrade, and it never needs a remove. The SSID is octets, not text, and a device compares it bytewise. A whole-table CMD_PROP_SET carrying two entries with the same SSID is refused with STATUS_INVALID_ARGUMENT before anything changes, under Mutation Atomicity: the value is an unordered set, so neither entry has standing to win, and a host that wrote both did not mean either.

The credential is write-only, under the rules of Provisioning Security. The reported form is the item through its SSID, and neither CMD_PROP_GET nor any notification ever carries a passphrase. This is why replacing an entry is the only way to change its credential: a host cannot read one back to compare it. Writes that carry a credential are subject to the same transport requirement as key material.

An insert that replaces an entry may instead keep its credential: when the SSID matches a stored entry, SECURITY is unchanged, and CREDENTIAL is empty, the device keeps the stored credential, in whatever form it was given, and takes only the new flags; bit 1 of such an insert is ignored. This is how a host that never held the passphrase changes whether a network joins on its own or is hidden. The insert does not disturb an association, and the device reports the merged entry. An empty credential with a changed SECURITY is refused with STATUS_INVALID_ARGUMENT, since a new mode needs a credential of its own, and an empty credential for a mode that takes one, naming an SSID not in the table, with STATUS_ITEM_NOT_FOUND: there is nothing to keep. A whole-table CMD_PROP_SET carries every credential.

A device bounds the table and refuses an insert past its capacity with STATUS_NOMEM. Four entries is enough for anything a device of this class does, and a device SHOULD hold at least that many. The bound MUST also keep the complete reported table, every entry in its redacted form, inside one frame on every transport the device exposes, so that a CMD_PROP_GET always answers in one piece. At under forty octets per reported entry that constrains nothing a device would want.

Removing the network in PROP_WIFI_NETWORK, whether by CMD_PROP_REMOVE or a whole-table CMD_PROP_SET that omits it, drops any association with it; the device publishes PROP_WIFI_NETWORK and the link as they change and chooses again. Replacing that network’s entry drops any association it holds and starts a fresh join with the new entry at once, because a host that has just corrected a passphrase should not wait out a backoff to learn whether it worked. Inserting an entry the device may choose, while it is looking for one, makes it a candidate at once.

The table is part of the saved snapshot, credentials included, which is what lets the device rejoin unattended. CMD_CLEAR erases the persisted copy with everything else and, as with everything else, leaves the live table alone: the device stays on its network until the CMD_RST that completes a factory reset reverts the table to its now-empty post-reset value. A device advertising CAP_SAVE stores it as it stores key material.

PROP 4882: PROP_WIFI_NETWORK

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI
  • Value Type: 1 to 32 octets (an SSID), or empty
  • Post-Reset Value: the network the station is on or joining; empty when there is none

The network the station is on, or is joining: the SSID of one entry in PROP_WIFI_NETWORKS, or empty when it is on none. The device fills it in as it chooses networks, and a host writes it to join one now or to leave. This is “connect” and “disconnect” both.

Writing an SSID joins that network. The device drops any current association and starts joining the new one, whether or not the entry is one it may choose by itself, and holds it: the device keeps trying that network, with backoff and whatever the failure, until the host writes something else, the station is disabled, the entry is removed, or the network has been joined and is then no longer heard, at which point the device chooses again. Writing the SSID already there disturbs nothing: an association up or in progress carries on, and a network the device chose becomes one it holds.

An SSID not in the table is refused with STATUS_ITEM_NOT_FOUND: the credential lives in the table, and this property only names it. A host acts on that differently from STATUS_INVALID_ARGUMENT, one being “insert it first” and the other a bug. Any write while the station is disabled answers STATUS_INVALID_STATE, since there is no station to join with and nothing is staged here; the device chooses a network when it is enabled.

Writing the empty value leaves the network and pauses the station: it stays up, scans on request, and joins nothing on its own until a host writes an SSID or the station is disabled and enabled again. A pause is reported as the empty value with PROP_WIFI_LINK at WIFI_LINK_DOWN. The empty value while the link is WIFI_LINK_CONNECTING is the device looking for a network to choose, so writing empty then is not a write of the value already there: it is a request to be on no network, which the device answers by pausing. A pause is live state. It is not saved, and a device that restarts chooses as it always does.

The device MUST publish the property whenever it changes without a write: a network it chose, a network it left, a removal from the table, or a picker on the device’s own screen.

PROP 4883: PROP_WIFI_SCANNING

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI_SCAN
  • Value Type: BOOL
  • Post-Reset Value: whether a scan is running

Whether a scan is in progress. Writing 1 starts one; the device answers with 1, reports each access point as it is heard through PROP_WIFI_SCAN_RESULTS, and publishes 0 when the scan completes. Writing 0 abandons a scan in progress and leaves in the results whatever had been found by then.

The frames of a scan MUST go out in this order, so that a host can never misattribute one:

  1. CMD_PROP_IS for PROP_WIFI_SCAN_RESULTS carrying the empty value: the previous results are gone.
  2. CMD_PROP_IS for this property carrying 1, as the reply to the host’s write or unsolicited when the device started the scan itself.
  3. One CMD_PROP_INSERTED per access point heard, in the order heard.
  4. CMD_PROP_IS for this property carrying 0, after the last of them.

The clear goes first so that every insert lands in a table the host knows to be empty, and the completion goes last so that a host seeing 0 holds the whole list without reading it.

This is a pairing window’s shape exactly: a state the host can enter, that ends by itself, and that a device with a screen can enter without the host. A device MUST bound a scan’s duration; a few seconds is what the hardware takes.

On a device with a station, a write of 1 answers STATUS_INVALID_STATE while the station is disabled, and MAY answer STATUS_BUSY while a join is in the middle of its handshake, which resolves by itself. On a device without one there is nothing to enable: the receiver is powered for the scan’s duration and put back to sleep after, which is the right power shape for a tracker that scans a few times an hour. Writing 1 during a scan succeeds and answers 1; there is nothing to restart. A write of 0 always succeeds.

A device MAY scan while associated, at the cost of the association’s traffic while it is off-channel. Whether the scan is active or passive, and on which channels, is the device’s business, except that a device with a station MUST probe by name for a hidden network in its table so that it can appear.

A scan the device starts itself, from its own menu or while looking for a network, is reported the same way, inserts and all. An attached host pays twenty-odd small frames once per scan, which is nothing against the scan itself, and a picker that appears as networks are heard is the difference between a list that fills in and a spinner.

PROP 4884: PROP_WIFI_SCAN_RESULTS

  • Type: Multiple-Value, Read-Only
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: Yes (Is, Inserted)
  • Required: CAP_WIFI_SCAN
  • Post-Reset Value: what the receiver has found; empty after a power-on

What the scan in progress has found so far, or what the last one found. Each item:

+-------+-----------+------+--------+-----------+
| MODES | FREQUENCY | RSSI | BSSID  |  SSID ... |
+-------+-----------+------+--------+-----------+
  2 B     2 B, MHz    1 B     6 B      0-32 B

Figure: Scan result item format

MODES is a 16-bit little-endian set of the security modes the access point offers, bit n standing for mode n of the enumeration above. No bits at all means the device did not determine them: a receiver that reads beacon headers for their addresses has no reason to parse the security elements, and no real access point offers nothing, so the empty set is free to mean that.

A WPA2/WPA3 transition network sets both bits. An OWE transition network is two BSSs, a visible open one and a hidden OWE companion that the open one’s transition element names by BSSID and SSID; the device reads the element and reports the visible BSS with both the open and the OWE bit set, so the host sees one network offering two modes and picks between them like any other. The companion is reported as itself, under the SSID the element gave it, and nothing depends on it. A set rather than a single strongest value, because which of the offered modes the host should write depends on what the device can do, and the scan result is not the place to guess: the host writes the strongest bit it likes and steps down on STATUS_UNIMPLEMENTED.

FREQUENCY is the center frequency of the access point’s primary 20 MHz channel, in megahertz, little-endian. A frequency rather than a channel number because a channel number is ambiguous across bands and a frequency is not, and the number follows from the frequency in one line wherever a display wants it. The width of the operating channel is not reported: the device negotiates it at association, and nothing about it is needed to join.

RSSI is a signed dBm. SSID is the remainder of the item, and empty means no name was reported, because the network hides it or because the scanner does not read names; the BSSID is what distinguishes one nameless entry from the next. A device with a station always reads names, so on such a device empty means hidden. A picker treats the two alike in any case, since neither can be chosen by name.

One item per access point, keyed by BSSID, and never the device’s own: a device that is also an access point MUST NOT report itself. The device reports what it heard and nothing it inferred. Which of several access points make up one network is a question the host answers by grouping on SSID, and a host that wants the list a phone shows coalesces, keeps the strongest per name, and sorts. A host that wants to see every radio in the building has that too. Hidden networks need no special case, since an access point with no name still has an address.

The device reports each access point with CMD_PROP_INSERTED as it is heard, and hearing one again is another CMD_PROP_INSERTED under the same BSSID, which replaces the host’s entry. Replacement by key is what makes the inserts idempotent, and idempotence is what makes it safe for a host to read the table mid-scan and follow the inserts from there: an item that arrives in both the reply and a notification is the same item twice.

The device retains a bounded table and reports an unbounded scan. Every access point heard is inserted; the device keeps the strongest of them up to its bound, evicting the weakest as stronger ones arrive, and never reports an eviction. The bound MUST be chosen so that the whole retained value fits in one frame on every transport the device exposes; on BLE that is the 512-octet reassembled frame, into which twenty or so typical items fit. The inserts are one item each and never approach it.

This is the one place the property model bends on purpose. A host that followed the inserts holds a superset of what a CMD_PROP_GET returns: everything the scan heard, against the strongest twenty the device kept. It is harmless because nothing in the host’s copy is invented, every item in it was heard, and the two are reconciled by the clear at the next scan. The alternative, a CMD_PROP_REMOVED per eviction, would spend frames telling the host to stop showing an access point it can see, in order to keep two views identical that nobody needs to compare. Where removals do describe something a host wants, as they do for access point clients, they are sent.

CMD_PROP_GET returns the retained table strongest first. Inserts arrive in the order the access points were heard, which is the order the host receives them in and has no other meaning.

Delivering results as they are found is a promise about delivery, not about pace. Most stacks hand back a scan only when it finishes; a device gets progressive results by scanning a channel at a time, which the usual scan interfaces allow and which costs a little total duration for first results in a fraction of a second. A device that cannot do that emits every insert at the end and conforms.

The value is cleared, and the empty value published with CMD_PROP_IS, when a scan starts and when the station is disabled. A list of what was in the air somewhere the device may no longer be is worse than an empty one.

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI
  • Value Type: structure below
  • Post-Reset Value: what the station is doing
+-------+--------+--------+-----------+
| STATE | REASON | BSSID  | FREQUENCY |
+-------+--------+--------+-----------+
  1 B     1 B      6 B      2 B, MHz
                 (present only when STATE is WIFI_LINK_UP)

Figure: Link state format

STATE:

ValueNameMeaning
0WIFI_LINK_DOWNNot trying: the station is off, it is paused, or no known network is one it may choose
1WIFI_LINK_CONNECTINGTrying: joining the network in PROP_WIFI_NETWORK, or looking for one to choose while that is empty
2WIFI_LINK_UPAssociated

REASON says why the device is in WIFI_LINK_CONNECTING rather than WIFI_LINK_UP, and is 0 in the other two states. It describes the network in PROP_WIFI_NETWORK, or, while that is empty, the search:

ValueNameMeaning
0WIFI_REASON_NONENo attempt has failed yet
1WIFI_REASON_NOT_FOUNDThe network was not heard and did not answer a probe; while looking, no network the device may choose was
2WIFI_REASON_AUTHThe network rejected the credential
3WIFI_REASON_REJECTEDThe network refused the association for another reason
4WIFI_REASON_LOSTThe association was up and dropped
5WIFI_REASON_OTHERSomething the device has no name for

WIFI_LINK_UP is an 802.11 statement: the station is authenticated and associated. Whether the device has an address on the link it is now on is a different layer’s fact and belongs to the family state properties, which report it.

WIFI_LINK_CONNECTING is the whole of trying, including the waits between attempts. While the station is enabled, not paused, and holding a network or knowing one it may choose, the device MUST retry indefinitely with backoff and MUST NOT give up: a wrong passphrase is a device that retries a few times an hour until someone fixes it, which costs nothing and is what unattended infrastructure should do. WIFI_LINK_DOWN is reserved for the states in which the device is not trying at all, so that a host reading DOWN knows the fix is configuration and a host reading CONNECTING knows the fix is in the reason.

The device MUST publish the property on every change of state and on every change of reason, and on nothing else. Retrying and failing the same way again is not a transition and is not published, so a device with a wrong passphrase reports WIFI_REASON_AUTH once, not every attempt.

When WIFI_LINK_UP, the value carries the association: which access point, and the center frequency of its primary channel in megahertz, as in a scan result. A roam to another access point of the same network is a change of value and is published.

Live state: NOT part of the saved snapshot, and CMD_RST reaches it only as a consequence of reverting the configuration it follows.

PROP 4886: PROP_WIFI_RSSI

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_WIFI
  • Value Type: INT8 in dBm, or empty
  • Post-Reset Value: the current measurement; empty when the link is not up

The received signal strength of the current association, measured when the property is read. Empty when the link is not up.

Kept out of PROP_WIFI_LINK for the reason PROP_GNSS_LOCATION is kept quiet: a measurement that changes on every beacon has no business in a property that is published on every change. PROP_PHY_RSSI is the same split on the LoRa side.

PROP 4887: PROP_WIFI_MAC

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: CAP_WIFI
  • Value Type: 6 octets

The station’s MAC address, as it appears to the access point. Constant because a router’s allow list is keyed on it, and a device that randomized it would be reporting an address nobody can use.

PROP 4912: PROP_WIFI_AP_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI_AP
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether the access point is up. Set, the device beacons the configured network, accepts stations onto it, and serves them addresses; cleared, it MUST disassociate every client, stop beaconing, and release whatever the access point held of the radio. The configuration is untouched in both directions.

Off by default, and a write of 1 while PROP_WIFI_AP_CONFIG is empty answers STATUS_INVALID_STATE. Together those two rules mean there is no factory network: no default name a stranger can look up, no default passphrase, and no open network a device falls back to because nobody configured one. A device that has never been given a network to offer offers nothing.

A write of 1 answers STATUS_INVALID_STATE on a platform that cannot run the access point alongside a radio it currently has on, the station included, and the device MUST NOT take the other down to honor the write. A platform that runs both, which is most of them, answers the write like any other.

Asynchronous for the reason every switch here is: a device with a screen MAY offer it, and the device takes the access point down by itself when its configuration is cleared.

PROP 4913: PROP_WIFI_AP_CONFIG

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_WIFI_AP
  • Value Type: structure below, or empty
  • Reported Form: the structure with its credential omitted
  • Post-Reset Value: Empty, or restored from saved state
+-------+----------+-------------+-----------+---------+--------+----------+--------+----------------+
| FLAGS | SECURITY | MAX_CLIENTS | FREQUENCY | ADDRESS | PREFIX | SSID_LEN |  SSID  | CREDENTIAL ... |
+-------+----------+-------------+-----------+---------+--------+----------+--------+----------------+
   1 B      1 B         1 B        2 B, MHz     4 B       1 B       1 B      1-32 B  security-defined

Figure: Access point configuration format

FLAGS, SECURITY, SSID_LEN, SSID, and CREDENTIAL are as in a PROP_WIFI_NETWORKS item, with the same encodings, the same well-formedness rules, and STATUS_INVALID_ARGUMENT for the same faults. The modes an access point may offer are WIFI_SEC_OPEN, WIFI_SEC_OWE, WIFI_SEC_WPA2, and WIFI_SEC_WPA3; any other is refused with STATUS_INVALID_ARGUMENT, because no credential form for offering it is defined here, and a device without SAE refuses WIFI_SEC_WPA3 with STATUS_UNIMPLEMENTED as it does in the table. A hidden network beacons without its name and answers probes for it; the flag is a courtesy to neighbors’ pickers and not a secret, since the name is in every association.

MAX_CLIENTS is how many stations the device admits at once, or 0 for the device’s own limit. A device bounds it to what its stack can hold and to what keeps PROP_WIFI_AP_CLIENTS in one frame, and clamps a larger write to that bound rather than refusing it, reporting the clamped value: the host asked for “many”, and the device’s most is the honest answer.

FREQUENCY is the center frequency of the primary channel the access point is to use, in megahertz, or 0 for the device’s choice. It is a preference, not a promise. A frequency the device may not use under its regulatory configuration is refused with STATUS_INVALID_ARGUMENT, and one it may use is what the access point beacons on while the station is not associated. On hardware where the access point and the station share one radio, and therefore one channel, the access point sits on the station’s channel while the station is up, whatever this field says, and moves when the station roams. PROP_WIFI_AP_STATE reports where the access point actually is.

ADDRESS and PREFIX are the device’s own IPv4 address on the network it offers and the prefix of that network, or all-zero and 0 for the device’s default, which SHOULD be 192.168.4.1/24 since that is what a phone joining an embedded device has come to expect. The device serves DHCP on this subnet, handing out addresses within the prefix other than its own, and answers as the gateway and resolver a lease names, whether or not it can forward anything. The prefix is 8 to 30. An address that is not a usable unicast address in the sense PROP_IPV4_STATE defines, or one that lies within the subnet the station holds, is refused with STATUS_INVALID_ARGUMENT. IPv6 on the offered network is link-local only, which needs no configuration and is not reported.

This is where the access point’s addressing lives, and not in the IP Connectivity properties, which describe the interface the device joins a network with and only that. Those properties configure how a device gets onto someone else’s network; the access point’s subnet is not a configuration the network hands the device but one the device imposes, it never changes without a write here, and a fixed address in the same structure as the network it belongs to is the shape every embedded access point already has.

The credential is write-only under Provisioning Security, and the reported form stops at the SSID. This is a passphrase the operator hands to other people, which makes a case for reading it back, and the case loses. The reader is a party that could write any passphrase it liked, so reading one back leaks nothing but the ability to rotate it without disturbing clients; a device with a screen MAY show it or a QR code for it; and one rule for every credential on the device is worth more than that convenience. It follows that every write of a secured configuration carries its credential, since there is nothing to leave in place.

A write takes effect at once. While the access point is up, the device disassociates every client, brings the network up again under the new configuration, and publishes the client list going empty; a client that knows the new credential rejoins on its own. Writing the empty value clears the configuration and is refused with STATUS_INVALID_STATE while the access point is enabled, so that no write here ever takes the network down as a side effect. The host disables it first.

Saved with the switch, credential included, so that a device commissioned to offer a network offers it at every boot.

PROP 4914: PROP_WIFI_AP_STATE

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_WIFI_AP
  • Value Type: structure below
  • Post-Reset Value: what the access point is doing
+-------+-----------+
| STATE | FREQUENCY |
+-------+-----------+
  1 B     2 B, MHz
        (present only when STATE is WIFI_AP_UP)

Figure: Access point state format

STATE:

ValueNameMeaning
0WIFI_AP_DOWNNot beaconing: disabled, unconfigured, or refused the radio
1WIFI_AP_UPBeaconing and accepting stations

FREQUENCY is the center frequency of the primary channel the access point is beaconing on, which is the configured one while the station is not associated and the station’s while it is. This is the field the property exists for: the configuration says what was asked, and only the state says where the network actually is, which a host that wants to tell a person which channel to expect has no other way to learn.

The device MUST publish it on every change of state and every change of frequency, so that a station roam which drags the access point to a new channel is reported, and on nothing else. Two states and no reason, because every way of being down is readable from the two configuration properties and the station’s link, and a reason octet would only quote them.

PROP 4915: PROP_WIFI_AP_CLIENTS

  • Type: Multiple-Value, Read-Only
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: Yes (Is, Inserted, Removed)
  • Required: CAP_WIFI_AP
  • Remove Selector: the MAC
  • Post-Reset Value: the stations currently associated; empty when the access point is down

The stations on the device’s network. Each item:

+-------+------+---------+
|  MAC  | RSSI | ADDRESS |
+-------+------+---------+
   6 B    1 B     4 B

Figure: Access point client item format

MAC is the client’s, and the key. RSSI is a signed dBm as the device last heard the client. ADDRESS is the IPv4 address the device’s DHCP server leased it, or all-zero until it has one, which is what lets a host reach a client it can see.

The device publishes CMD_PROP_INSERTED when a station associates and again, under the same MAC, when its address is leased or changes, which replaces the host’s entry as a scan result does; CMD_PROP_REMOVED with the MAC when a station leaves or is dropped; and CMD_PROP_IS with the empty value when the access point goes down. RSSI is reported as it stood at the last of those and is not published on its own, under the rule that keeps a beacon-rate measurement out of every published property here.

This is the one multi-value property in this chapter that emits CMD_PROP_REMOVED, and the difference from PROP_WIFI_SCAN_RESULTS is not a matter of taste. The scan table withholds removals because they would describe an eviction from a bounded view of an unbounded list, telling the host to stop showing something real. A client leaving is a fact about the world that the host wants, the list is bounded by MAX_CLIENTS to something small, and the device’s view and the host’s are meant to be the same view, so the ordinary property model applies without amendment. The bound MUST keep the whole list in one frame, which at eleven octets an item constrains nothing.

Choosing a Network

While the station is enabled, not paused, and holding no network, the device chooses one. Its candidates are the entries in PROP_WIFI_NETWORKS without FLAGS bit 2; it MUST NOT join any other entry on its own. A network a host writes to PROP_WIFI_NETWORK, or one picked on the device’s own screen, is held instead, and nothing in this section applies to it until the hold ends.

Which one. Of the candidates heard, or for a hidden one answering a probe, the device SHOULD prefer the one it was associated with most recently, then the strongest. A device with storage SHOULD remember which that was across a power cycle; the memory is the device’s own and not a property. The order of the table means nothing.

Staying. The device MUST NOT leave a working association for another candidate, however much stronger it is. It chooses again only when the link is lost. A move between access points of one network is a roam within it, reported as a change of BSSID.

Failing. When a join fails, the device tries the next candidate heard and returns to the one that failed after its backoff; a candidate that has failed ranks after any that have not. With one candidate this is retrying that network indefinitely, as PROP_WIFI_LINK requires. PROP_WIFI_NETWORK names whichever candidate the device is attempting and is published as that changes, so a host reads each failure against the network it belongs to.

Looking. With no candidate heard, PROP_WIFI_NETWORK is empty, the link is WIFI_LINK_CONNECTING, and the device scans for candidates, probing by name for hidden ones. It backs off between scans to an interval a battery can bear and a mesh radio sharing the antenna can afford; the interval is the device’s business.

On a wall. A device meant to stay on one network is given one candidate, alone or among entries with FLAGS bit 2 set. What it does then depends on nothing in the air but that network.

Sharing the Radio

One radio, one channel. Where the access point and the station are two interfaces on one transceiver, the transceiver is on one channel. While the station is associated the access point is on the station’s channel; when the station roams, the access point moves with it and its clients drop and rejoin, which is a few seconds of disruption the device did not choose and cannot avoid, and which PROP_WIFI_AP_STATE reports as a frequency change. A host that needs the access point to stay put keeps the station off, or on a network with one access point.

A scan is a gap. Scanning takes the transceiver off channel. Every client of the access point loses its beacons for the duration, which most stations tolerate and some do not, and an association loses its traffic. On a device whose Wi-Fi receiver is its LoRa transceiver, a scan is also a gap in mesh reception lasting as long as the scan does, during which nothing on the air is heard and nothing is forwarded. The host asked for that, and it is not free: a repeater’s operator scanning on a schedule is spending the mesh’s reliability. A device MAY answer STATUS_BUSY to a scan request rather than abandon a transmission in progress or drop the clients it is serving.

Enable in either order. The two switches are independent, and a platform that cannot run both refuses the second 1 with STATUS_INVALID_STATE and leaves the first alone. Nothing about the station’s table, network, or link changes when the access point comes up or goes down, and nothing about the access point’s configuration changes with the station’s.

Forwarding is not specified. Whether a client of the access point can reach the network the station is on, and how, is a policy about what the device does with its two interfaces. A device that forwards nothing is a conforming access point, and a useful one to a phone that only wants to reach the device.

Regulatory. The channel set follows the platform’s regulatory configuration, with one asymmetry: a station on the wrong channel is a receiver that hears nothing, and an access point on the wrong channel is a transmitter beaconing where it may not. A device advertising CAP_WIFI_AP therefore MUST refuse a FREQUENCY outside its regulatory configuration, and MUST choose within it when the field is 0.

Host Procedures

The flows a host runs, in terms of the properties above. None of them needs anything the property grammar does not already provide.

Turn on. Write PROP_WIFI_ENABLED to 1. If the table holds a candidate, PROP_WIFI_LINK publishes CONNECTING, PROP_WIFI_NETWORK names the network the device tries, and the link then publishes UP or a reason.

Scan. Write PROP_WIFI_SCANNING to 1; clear the list on the CMD_PROP_IS that follows; add or replace an entry per CMD_PROP_INSERTED, grouping by SSID for display; stop the spinner on the unsolicited 0. A host on a binding that carries no notifications polls PROP_WIFI_SCANNING and reads PROP_WIFI_SCAN_RESULTS once it reads 0. A host presenting a picker marks each network already in PROP_WIFI_NETWORKS by matching SSIDs, since the reported table form carries them.

Join a new network. Insert an entry in PROP_WIFI_NETWORKS with the SSID from the scan result, the strongest mode in its offered set, and the passphrase from the operator; on STATUS_UNIMPLEMENTED, insert again with the next mode down. Then write the SSID to PROP_WIFI_NETWORK to join it now rather than when the device next chooses. Two writes rather than one because the credential has one home and PROP_WIFI_NETWORK only names it. There is no safe one-frame shortcut: a whole-table CMD_PROP_SET replaces every entry, and since credentials cannot be read back, a host can only write a whole table it holds every credential for. That is the first host commissioning a fresh device, and nobody after it.

Fix a wrong passphrase. PROP_WIFI_LINK reads CONNECTING with WIFI_REASON_AUTH. Insert the entry again with the corrected credential; the device tries it immediately and the link reports the outcome.

Switch networks. Write the other SSID to PROP_WIFI_NETWORK.

Join only by hand. Insert the entry with FLAGS bit 2 set. The device joins it when it is written to PROP_WIFI_NETWORK and never chooses it. To change a saved entry either way, insert it again with its SSID, its SECURITY, the new flags, and an empty credential, which keeps the passphrase the host never had.

Reconnect. There is no such flow. The device is already retrying, and a host that wants a fresh start with the same credentials has the honest two-step: write empty, then the SSID, each of which is a real state.

Disconnect. Write PROP_WIFI_NETWORK empty. The device leaves and pauses with the station still up, until a network is written or the station is turned off and on.

Forget. Remove the entry from PROP_WIFI_NETWORKS. If the device was on it, the link drops and the device chooses again, all published.

Turn off. Write PROP_WIFI_ENABLED to 0. Nothing is forgotten.

Locate. On a device with CAP_WIFI_SCAN and no station, run the scan flow and hand the BSSIDs and signal levels to whatever resolves them into a position. The item format is what a geolocation resolver consumes, and a phone passes the list straight through. What the device does with its own scan, resolving on board or sending access points over the air, is application and is not here.

Offer a network. Write PROP_WIFI_AP_CONFIG with the SSID, mode, and credential, then PROP_WIFI_AP_ENABLED to 1. Watch PROP_WIFI_AP_STATE for the channel it landed on and PROP_WIFI_AP_CLIENTS for who arrives.

Commission for unattended use. Insert the networks, enable the station or the access point, then CMD_SAVE. The station comes up and joins a candidate, and the access point comes up, at every boot after that.

Synchronize on attach. Read PROP_WIFI_ENABLED, PROP_WIFI_NETWORK, PROP_WIFI_LINK, and, where the device has one, PROP_WIFI_AP_ENABLED and PROP_WIFI_AP_STATE, in one CMD_PROP_MULTI_GET where available. The tables are read only when the host needs to show them.

What Survives What

PropertySavedCMD_RSTDetachStation off
PROP_WIFI_ENABLEDyesrevertskept—
PROP_WIFI_NETWORKSyesrevertskeptkept
PROP_WIFI_NETWORKnofollowskeptempty
PROP_WIFI_SCANNINGnofollowskeptabandoned
PROP_WIFI_SCAN_RESULTSnofollowskeptcleared
PROP_WIFI_LINKnofollowskeptDOWN
PROP_WIFI_RSSInofollowskeptempty
PROP_WIFI_MAC————
PROP_WIFI_AP_ENABLEDyesrevertskept—
PROP_WIFI_AP_CONFIGyesrevertskept—
PROP_WIFI_AP_STATEnofollowskept—
PROP_WIFI_AP_CLIENTSnofollowskept—

“Reverts” means to the post-reset value, which on a device with a snapshot is the saved one. “Follows” means the live state ends up wherever the reverted configuration puts it, and nowhere else. A CMD_RST on a device whose live configuration already matches its snapshot leaves a running scan running, an association up, and an access point beaconing; one that reverts the entry of the network the station is on drops the association and joins with the restored entry, or chooses again if none was restored; and one on a device with no snapshot takes the station and the access point down and everything live with them. A held network and a pause outlast either command unless it removes the held entry, which ends the hold, or turns the station off, which ends both.

CMD_RESTORE is the same column: in either of its forms it reverts the configuration properties to the snapshot and the live state follows. Detach touches nothing, because the station and the access point are device-domain and run unattended, and a scan that was in progress when the host left completes and leaves its results for the next one.

Over the Node Management Binding

Every property here is device-domain, so a listed administrator over the mesh may read all of them and write the configuration ones, under the same rule that lets channel keys be provisioned remotely: the binding already delivers each request authenticated and encrypted, so an administrator may provision a credential.

Reading is the redacted form and nothing else, here as on the local link. The network table and the access point’s configuration read back without their credentials, and no binding exists over which a credential can be read.

That binding carries no unsolicited notifications, so a scan is the polled flow above, and an administrator that wants the client list reads it while PROP_WIFI_AP_STATE says there is one to read. Scan results are the one large read, and the binding’s cursors carry them.

An administrator who disables the station a bridge tunnel rides on has done the same thing as one who writes PROP_MAC_BACKHAUL, and warrants the same warning in the same place.

On BLE

The frame-size concerns are a CMD_PROP_GET of the scan results, of the network table, and of the client list, and all three are bounded above so that they fit. The inserts that deliver scanned access points live are one item each. The largest credential write, a 32-octet SSID with a 128-octet SAE password, is under two hundred octets with framing. Everything else is a few.

Security Considerations

  • A passphrase crosses the link once, inbound, over a transport that meets the provisioning requirement, and is never reported. A later host on the same device cannot extract an earlier host’s Wi-Fi credentials any more than its channel keys.
  • The station’s security requirement is a minimum constrained by its credential. Stronger compatible modes are permitted; weaker modes require an explicit host write. A WPA3 requirement never falls back to WPA2, and an OWE requirement never falls back to open.
  • Scan results are what the device heard, and an SSID is what its sender chose to call itself. A host displays them as untrusted strings.
  • A candidate with no credential, open or OWE, is joined wherever anything calls itself by that name. A host that saves one for a single visit sets FLAGS bit 2, or forgets it after.
  • A scan result list is a location fingerprint of wherever the device is standing, precise to a building, which is exactly why a tracker wants one. It is readable only by an admitted party, like everything else here, and a device that resolves positions on board treats what it learned the way it treats a fix.
  • An access point is the one thing here that announces the device to everyone in range. Its SSID and its MAC are in every beacon, a hidden network is hidden from pickers and not from anyone listening, and the name it beacons MUST NOT carry the device’s mesh identity or any part of its address. There is no factory network: the access point offers nothing until a host configures it, and its passphrase is write-only like every other credential.
  • PROP_WIFI_MAC and the SSIDs in the table are identifying. They are no more so than PROP_DEV_NAME, and they are readable only by a party that has already been admitted.
  • A device on Wi-Fi is a device on a LAN, and a device with an access point is a device hosting one. This chapter gives it an address and nothing that listens on one. Any service the device later offers over the interface, a ULCP binding over TCP above all, carries the full authority of an attached host and needs an admission ceremony of its own before it exists; the serial transports’ physical-possession argument does not extend to a network port, and it extends least of all to a network the device invited the client onto. Whether knowing the access point’s passphrase is itself such a ceremony is a decision for the binding that would rely on it.

Not Specified

Deliberately absent, with the reason:

  • Anything on the access point’s network. Forwarding between the offered network and the station’s, a captive portal, a provisioning flow, or a binding a client could attach through: each is a thing the device does with the network rather than the network itself, and the last needs the admission ceremony above before it can exist.
  • Enterprise authentication. Certificates and identities are a provisioning surface an order of magnitude larger than a passphrase. The three enterprise modes have numbers so that scan results can name them and a host can explain why a network is unavailable; the credential forms come with the capability that opens them.
  • Negotiation details. Management frame protection, SAE hash-to-element, transition-disable: the device does what the mode requires and the host never sees them. WPS and Easy Connect are provisioning methods rather than modes.
  • A priority order. The device prefers the network it was on last, then the strongest. A ranked table would need a field in every entry and a reordering surface on every host, to settle a choice that only arises where two known networks overlap.
  • Regulatory country. The channel set follows the platform’s regulatory configuration, which may later be tied to the device’s region; nothing here decides how that configuration is set. What is decided is that an access point stays inside it.
  • Power-save mode, PHY rate, band preference. The device’s business. A property that exposes them is easy to add and hard to remove.
  • A reconnect request. The one genuinely command-shaped act in the vicinity is deliberately not disguised as a property: a property write with a side effect when written with the value it already holds is a command in a costume, and it breaks the moment a host replays its configuration. It is also unnecessary, since the device retries on its own and replacing the entry of the network it is joining restarts the join at once.

ULCP: IP Connectivity

The layer above a link: whether the device can reach anything, what it is reachable at, and how to configure the cases the network does not configure for it.

Address configuration is not a property of Wi-Fi. A wired link, should one ever appear, needs exactly these properties, and a device’s IP stack is one thing whichever link carries it. Everything here is therefore named IP rather than after a link, and nothing in it knows what the link is.

Nothing about what the device reaches belongs here. A bridge tunnel or a time source reports its own state under its own capability.

These properties describe one interface: the one the device uses to join a network, which is the station’s on a device whose only link is Wi-Fi. A device’s own access point is a second interface and is deliberately not described here, because its addressing is a subnet the device imposes rather than one a network hands it, and it lives in that access point’s own configuration. A device with a second link of the joining kind is a future revision, and these properties are shaped so that an interface selector could be added without renaming them.

The two configuration properties and the configured resolver list are device-domain state and part of a saved snapshot. The five that report what the stack currently holds are live: never saved, and reached by CMD_RST only through the configuration they follow.

Capabilities

CodeNameRequiresGrants
55CAP_IPV4—An IPv4 stack on the device’s link: PROP_IPV4_STATE, PROP_IPV4_CONFIG, PROP_IPV4_ADDRESS, and the shared PROP_IP_DNS and PROP_IP_RESOLVERS
56CAP_IPV6—An IPv6 stack on the device’s link: PROP_IPV6_STATE, PROP_IPV6_CONFIG, PROP_IPV6_ADDRESSES, and the same two shared properties

One capability per family, because the families are peers. The BLE binding argues for a single capability on the grounds that a refusal is a complete answer about an extra, and neither family is an extra to the other: a device that speaks only IPv4 is ordinary today, and a device that speaks only IPv6 is an ordinary device on an IPv6-only network tomorrow. Making either the floor would encode which one is normal, which is a fact about the year rather than about the protocol.

A device with both advertises both, and a host that sees either knows the two shared properties are there. The seam stops at the family: DHCP versus static, advertisements versus DHCPv6, are methods within a family, and a device that lacks one refuses the write with STATUS_UNIMPLEMENTED as that argument intends.

Neither capability formally requires a link capability, on purpose. A capability’s requirements are concrete codes, and naming CAP_WIFI here would make a wired device either lie about having Wi-Fi or invent a second pair of IP capabilities. The precondition is stated instead: a device advertising either has a stack on one link, and describes that link through whatever link capability it also advertises. A device that advertises an IP capability and no link capability has a link it offers no control over, which is a legal shape for a device with a fixed wired port; its family states simply never report IP_NO_LINK for a reason the host can act on.

Properties

Allocated in the block after the Wi-Fi station’s.

All IPv4 and IPv6 addresses, including gateways and resolvers, are encoded as octets in network byte order (big-endian).

IdMnemonicCommandsDescription
4896PROP_IPV4_STATEGet, IsIPv4 readiness
4897PROP_IPV4_CONFIGGet, SetHow IPv4 is configured
4898PROP_IPV4_ADDRESSGet, IsThe IPv4 address, prefix, and gateway in effect
4899PROP_IPV6_STATEGet, IsIPv6 readiness
4900PROP_IPV6_CONFIGGet, SetHow IPv6 is configured
4901PROP_IPV6_ADDRESSESGet, IsThe IPv6 addresses and default routers in effect
4902PROP_IP_DNSGet, Set, Insert, RemoveConfigured resolvers, or empty to use what the network provides
4903PROP_IP_RESOLVERSGet, IsThe resolvers in use

4904 through 4911 are reserved for this subsystem.

PROP 4896: PROP_IPV4_STATE

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_IPV4
  • Value Type: UINT8
  • Post-Reset Value: what the family is doing

One octet, from the enumeration both family states share:

ValueNameMeaning
0IP_DISABLEDThe family is configured off
1IP_NO_LINKThe link is down, so there is nothing to address
2IP_WAITINGThe link is up and the family has no usable address yet
3IP_READYThe family holds a usable address
4IP_CONFLICTThe configured static address is held by something else

A usable address is a unicast host address that is not link-local: for IPv4 anything outside 169.254/16 that is neither multicast, broadcast, loopback, nor unspecified, and for IPv6 anything outside fe80::/10 under the same exclusions. A self-assigned 169.254 address reaches only the link, exactly as fe80:: does, and a device that fell back to one is a device whose DHCP failed, which IP_WAITING says and IP_READY would hide. A device with only an IPv6 link-local address is IP_WAITING, because it is waiting for exactly the advertisement that would give it a usable one. This is the boundary readiness is defined on, and the same boundary the static configuration is validated against.

This is the property a host watches for its family. It changes when a lease is obtained or lost, when a router starts or stops advertising, and when the link comes and goes, which is a few times in a session rather than a few times a minute. The device MUST publish it on any change and on nothing else. The address and resolver properties publish their own changes, since those can move while the state stands still.

IP_WAITING is this layer’s WIFI_LINK_CONNECTING: the device is doing what its configuration says and the network has not answered. It carries no reason, because the reasons are the network’s, a DHCP server that does not answer or a router that does not advertise, and the fix is on the network.

IP_CONFLICT is the exception that earns its own value. A static address that duplicate-address detection or an ARP probe finds already in use is a fault in the configuration this protocol wrote, the fix is a different address, and a host that could not tell it from an ordinary wait would tell the operator to check the router. The device keeps probing while in IP_CONFLICT and moves to IP_READY if the other holder goes away. Under IP_METHOD_AUTO a conflict is the stack’s to resolve, by declining the lease and asking again, and the family stays IP_WAITING.

IP_NO_LINK is separate so that a host can tell “the station is not associated” from “the station is associated and nobody is handing out addresses” without reading PROP_WIFI_LINK as well.

IP_READY means an address, not a route. Whether the family also has a default route is in the address property, the gateway field for IPv4 and the router items for IPv6, and a device on an isolated network that hands out addresses and no gateway is ready by this definition, which is the honest one.

PROP 4897: PROP_IPV4_CONFIG

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IPV4
  • Value Type: structure below
  • Post-Reset Value: IP_METHOD_AUTO, or restored from saved state
+--------+---------+--------+---------+
| METHOD | ADDRESS | PREFIX | GATEWAY |
+--------+---------+--------+---------+
   1 B      4 B       1 B      4 B
         (present only when METHOD is IP_METHOD_STATIC)

Figure: IPv4 configuration format

METHOD:

ValueNameMeaning
0IP_METHOD_DISABLEDThe family is not used on the link
1IP_METHOD_AUTODHCP
2IP_METHOD_STATICThe address, prefix, and gateway that follow

The default is IP_METHOD_AUTO, so that a device with nothing configured is on the network the moment it is associated; this subsystem exists for the cases where that is not enough.

A PREFIX above 32, a static form of the wrong length, or a static address that is not usable in the sense PROP_IPV4_STATE defines, 169.254/16 included, is refused with STATUS_INVALID_ARGUMENT. A GATEWAY is either all-zero or a unicast address that is not multicast, broadcast, loopback, or link-local, and anything else is refused the same way; the IPv6 form relaxes the last exclusion, since a router names itself by its link-local address and a static IPv6 gateway is usually exactly that. All-zero means no default route, which is what a device on a network with no way out should be told.

A write takes effect at once. On a device that is IP_READY under the old configuration, the old address is released and the new one applied, and PROP_IPV4_STATE reports the transitions like any other. A write while the link is down is accepted and waits for it, so that a static configuration can be staged before the link is enabled.

PROP 4898: PROP_IPV4_ADDRESS

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_IPV4
  • Value Type: 4-octet address, 1-octet prefix, 4-octet gateway; or empty
  • Post-Reset Value: the address in effect; empty when the family is not IP_READY

The IPv4 address the interface holds, its prefix length, and the default gateway, all-zero when there is none. Whatever the method: under IP_METHOD_AUTO this is what the lease said, and under IP_METHOD_STATIC it is what was written, once the device holds it.

The device MUST publish it whenever the reported value changes. Mostly that is when PROP_IPV4_STATE moves, but not only then: a lease renewal can keep the address and change the gateway, and a host that read the value once and watched only the state would carry the old gateway forever. A renewal that changes nothing publishes nothing.

PROP 4899: PROP_IPV6_STATE

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_IPV6
  • Value Type: UINT8
  • Post-Reset Value: what the family is doing

IPv6 readiness, from the enumeration PROP_IPV4_STATE defines, with the same publication rule and the same meaning for every value. The one family-specific note is the link-local boundary: a device holding only an fe80:: address is IP_WAITING, because it is waiting for exactly the advertisement that would give it a usable one.

Two properties rather than two octets in one, because the capabilities are two. A device without CAP_IPV6 would otherwise carry an octet describing a family it does not have, and a property granted by “either capability” is a property with two homes. One octet per family costs a second notification when a link drop takes both families down, which is one small frame at a moment the host is already being told things.

PROP 4900: PROP_IPV6_CONFIG

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IPV6
  • Value Type: as PROP_IPV4_CONFIG with 16-octet addresses
  • Post-Reset Value: IP_METHOD_AUTO, or restored from saved state

The same structure and the same methods, with 16-octet addresses and a prefix of at most 128. A static address that is not usable in the sense PROP_IPV6_STATE defines, link-local, multicast, loopback, or unspecified, is refused with STATUS_INVALID_ARGUMENT; a static address that is usable but already held on the link is accepted and reported as IP_CONFLICT.

IP_METHOD_AUTO means router advertisements, and DHCPv6 where the router asks for it. Which of those produced an address is not reported, because a host has nothing to do with the difference.

A device MUST hold a stable address and MUST report only stable addresses. It MAY additionally use temporary addresses for the traffic it originates, which is the arrangement RFC 8981 describes for a host that is reached at one address and reaches out from others, and those are never reported: they exist to rotate, and a reported address is one somebody wrote down.

PROP 4901: PROP_IPV6_ADDRESSES

  • Type: Multiple-Value, Read-Only
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: Yes
  • Required: CAP_IPV6
  • Post-Reset Value: what is in effect; empty when the family is not IP_READY

The usable IPv6 addresses the device holds, and the default routers it has selected. Each item is a kind octet and a kind-defined body:

KindNameBody
0IPV6_ADDRESS16-octet address, 1-octet prefix length
1IPV6_ROUTER16-octet router address

A set rather than one address, because an IPv6 interface normally holds several, a global one and a unique-local one from separate prefixes, say, and which of them the device uses as a source depends on where the packet is going. There is no one answer to “the device’s address”, only “the addresses the device is reachable at”, which is what a host displaying or dialing it needs. Stable addresses only, per the configuration above. The link-local address is not among them, for the reason it does not make the family ready.

The router items are the stack’s default router list, every router it currently retains from those advertising, and none when there is none. A list rather than one, because a stack keeps several and may send to different destinations through different ones, so no single router describes the routing.

The prefix length is the one the address’s assignment carried: the advertised prefix an autoconfigured address was formed from, or the prefix written for a static one. An address assigned by DHCPv6 reports 128, and 128 means the assignment carried none rather than that the link is a /128: DHCPv6 assigns addresses, not prefixes, and the on-link prefixes a router advertises alongside are routing state that is not encoded here.

A device bounds the set to what its stack holds, which for an embedded stack is a few addresses and a few routers, and the bound MUST keep the complete value inside one frame on every transport the device exposes. Which entries a stack keeps once a network offers more than it can hold is the stack’s business.

The device MUST publish it whenever the reported set changes: a prefix renumbered, a router replaced or expired, an address added or withdrawn. Router and prefix lifetimes are independent of one another and of the state, so this property moves while PROP_IPV6_STATE stands still, and a host that only watched the state would not learn.

PROP 4902: PROP_IP_DNS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_IPV4 or CAP_IPV6
  • Item Form: a 4-octet IPv4 or 16-octet IPv6 resolver address
  • Remove Selector: the address
  • Post-Reset Value: Empty, or restored from saved state

The resolvers the device is to use. Empty, the default, means the ones the network provided, by DHCP or router advertisement. Non-empty, these replace the network’s rather than join them, because a host that configured resolvers meant those, and a merged set would be neither what it wrote nor what the network offered.

An item of any other length, or one that is not a unicast address, unspecified, multicast, broadcast, or loopback, is refused with STATUS_INVALID_ARGUMENT. An IPv6 resolver MAY be link-local, as one advertised by a home router commonly is, and an IPv4 one MUST NOT be. A device bounds the set, SHOULD hold at least two, and refuses past its capacity with STATUS_NOMEM.

The set is unordered, as every multi-value property is, and a device asks whichever it likes first.

Static addressing without this is a device that cannot resolve a name, which is why the two are written together.

PROP 4903: PROP_IP_RESOLVERS

  • Type: Multiple-Value, Read-Only
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: Yes
  • Required: CAP_IPV4 or CAP_IPV6
  • Item Form: as PROP_IP_DNS
  • Post-Reset Value: the resolvers in use; empty when there are none

What the device is resolving with right now: the configured set when PROP_IP_DNS is non-empty, otherwise what the network handed it, and empty when neither has given it anything.

The device MUST publish it whenever the set changes, for the same reason the addresses are published: a router advertisement carries resolvers with lifetimes of their own and can add one, replace one, or withdraw one with a zero lifetime, all without the family leaving IP_READY. Bounded as PROP_IPV6_ADDRESSES is, to what the stack holds and to one frame; a stack that keeps two or three resolvers is the norm, and a network offering more than that is offering more than the device will ask.

Chiefly a diagnostic, and the one that distinguishes “the device is on the network and cannot resolve the server’s name” from every other way a tunnel fails to come up.

On a device whose link is Wi-Fi, the two subsystems meet at exactly one property on each side: PROP_WIFI_LINK says whether there is a link, and the two family states say what each family has made of it. The address and resolver properties are read after one of those has moved and followed thereafter, since each publishes its own changes.

The stack follows the link. While the link is anything but up, every enabled family is IP_NO_LINK and both address properties are empty. When the link comes up, each enabled family goes to IP_WAITING and then, as the network answers, to IP_READY; a static family skips the wait except for the duplicate-address check, which ends in IP_READY or IP_CONFLICT. When the link drops, everything goes back to IP_NO_LINK and the addresses empty with it, learned resolvers included. A roam to another access point of the same network is the same link and does not disturb the stack: a device MUST NOT release its addresses over a roam.

Publication order is link first, then stack, in both directions. On the way up, PROP_WIFI_LINK carrying WIFI_LINK_UP, then each family’s state for each transition as it happens; on the way down, PROP_WIFI_LINK leaving WIFI_LINK_UP, then each family’s state carrying IP_NO_LINK. A host that sees IP_READY reads the addresses it wants and keeps them current from their own notifications; one that sees IP_WAITING persist while the link is up knows the problem is the network and not the radio; and one that sees IP_CONFLICT knows it is the configuration.

Disabling the link reports IP_NO_LINK, not IP_DISABLED. The second is the family’s own switch, written in its configuration, and a host that turned the radio off did not turn IPv6 off.

Configuration is staged in any order. These configuration properties are writable while the link is down and while the radio is disabled, and they are ordinary CMD_PROP_SET targets, so a static commissioning is one CMD_PROP_MULTI_SET where CAP_CMD_MULTI is present: PROP_IPV4_CONFIG, PROP_IP_DNS, PROP_WIFI_ENABLED, in that order, then CMD_SAVE. The network insert still stands apart, being an insert, and the station joins it on its own.

Synchronizing on attach adds the family states to the link’s read: PROP_WIFI_ENABLED, PROP_WIFI_NETWORK, PROP_WIFI_LINK, PROP_IPV4_STATE, and PROP_IPV6_STATE, in one CMD_PROP_MULTI_GET, where a family the device lacks comes back as the STATUS_PROP_NOT_FOUND entry the multi-get already provides for. Between them a host knows whether the device is configured, associated, and addressed without a second round trip.

Reconfiguring a live device is a write and a watch. The device applies the new configuration at once, the family leaves IP_READY and arrives wherever the new configuration leads, IP_READY again by way of IP_WAITING, IP_CONFLICT for a static address somebody else holds, or IP_DISABLED, and a host that changed a static address learns the old one is gone by the same notification everyone else does.

The host name the device presents to DHCP, and to DHCPv6 where it runs, SHOULD be derived from PROP_DEV_NAME, folded to a valid label, so that the device can be found on a router’s client list under the name its operator gave it.

What survives what. The three configuration properties are saved and revert with CMD_RST; the five live ones follow, in the sense the Wi-Fi chapter’s table defines. CMD_RST on a device whose snapshot matches its live IP configuration leaves a lease in place; one that reverts a static address releases and re-applies, with the transitions published.

Over the node management binding, all eight are device-domain: an administrator may read every one and write the three configuration properties, the other five being read-only for everybody. That binding carries no unsolicited notifications, so the read-then-follow flow does not apply across the mesh: an administrator that wants a current view of addresses or resolvers reads them again, and the family states tell it when that is likely to be worth doing. An administrator who writes a static address that is wrong for the network has stranded a bridge just as surely as one who disabled the link, and the same warning belongs in the same place.

Security Considerations

The IP stack trusts its network the way every client does. A rogue DHCP server or router advertisement on the LAN can hand the device a bad address, a bad route, or a resolver that lies. Nothing at this layer defends against that, and nothing needs to, because what rides on the address authenticates its far end itself: the bridge tunnel pins a key, and a redirected tunnel fails to open rather than opening to the wrong party.

Not Specified

  • A reachability test. Command-shaped, and unnecessary: a bridge client reports whether its tunnel is up, and that is the reachability anyone cares about.
  • A DHCP renew request. The same argument. A lease is the stack’s to manage, and a host that could force a renewal could not observe anything by it that the address property does not already publish.
  • Time from the network. It belongs to the time capability, which would gain a trust switch shaped like PROP_GNSS_TIME_TRUST if it takes SNTP.
  • A second interface. A selector for a later revision. The device’s own access point is not one of these interfaces; see PROP_WIFI_AP_CONFIG.
  • mDNS. A service the device offers rather than a property of its stack. A device in an IP multicast group advertises over it.

Bridge Client

An integrated internet bridge client connects the device’s node to a bridge server over IP. Configuration belongs to the device domain and is independent of the attached host. Enabling the client does not enable Wi-Fi, change IP configuration, or change PROP_MAC_REPEATER_ENABLED. While forwarding, a node with an enabled and configured client derives the role Bridge (see PROP_IDENT_ROLE).

Capabilities

CAP_BRIDGE_CLIENT (58) requires CAP_DEV_IDENTITY and at least one implemented IP family. It requires all five properties below. The IP-family requirement is an alternative expressed here in prose, not an AND-list of capability codes.

PROP_BRIDGE_ENABLED

Property 4928; Get/Set; BOOL. Defaults to false. A write changes the desired state immediately; it does not wait for a connection. When enabled without a host or server key, the client reports Unconfigured and makes no connection.

PROP_BRIDGE_HOST

Property 4929; Get/Set; STRING. A DNS A-label hostname or unbracketed IP literal, without a scheme, path, or port. Maximum 253 octets before the terminating NUL. Empty clears the host. A client uses the IP families its platform implements; an unsupported literal reports a connection failure rather than falling back to another destination. The hostname supplies TLS SNI where applicable; it does not establish trust in the server.

PROP_BRIDGE_PORT

Property 4930; Get/Set; UINT16. Defaults to 21837. Zero is invalid. Like other ULCP scalars, the value is little-endian.

PROP_BRIDGE_SERVER_KEY

Property 4931; Get/Set; a canonical 32-octet Ed25519 public key, or empty to clear the pin. Invalid or weak public keys are refused. The client authenticates the TLS 1.3 CertificateVerify signature against this key. Certificate claims, names, validity dates, and public certificate authorities do not establish trust.

The client’s authentication identity MUST be the device identity exposed by PROP_DEV_KEY. No additional client key is provisioned or exposed through ULCP. If that identity ceases to match the running node and TLS credential, the client stops until identity initialization completes.

Property 4932; Get/Is; two octets: STATE followed by REASON. Read-only. Changes are announced to the attached host. Enablement and connection state are distinct.

STATEMeaning
0Disabled
1Unconfigured
2Waiting for network
3Connecting
4Connected
5Retrying
REASONMeaning
0None
1DNS failure or unsupported address family
2TCP connection or I/O failure
3TLS protocol failure
4Authentication failure
5Receive idle timeout
6Device identity unavailable

Connected describes the tunnel, not whether the node repeats. A client whose repeater is disabled is a valid leaf; hosts should display the repeater state alongside the link state.

Persistence and lifecycle

The four configuration properties are saved by CMD_SAVE in the device snapshot. CMD_CLEAR erases saved configuration without changing live configuration; CMD_RST follows the normal saved-state restore rules. Defaults are disabled, empty host, port 21837, and no server key. Attach/detach does not change these properties or interrupt the bridge. Authorized mesh administrators may write them through the existing device-management binding.

Changing configuration or losing the network closes the current connection and discards queued frames. Each connection starts with empty queues and decoder state. Invalid writes leave the old value intact. Connection failures are reported through PROP_BRIDGE_LINK, not by delaying a configuration response.

IP Multicast

A device with this capability sends what it transmits to an IP multicast group on the local network and receives what is sent there. It is one of two kinds:

  • A forwarding device has a LoRa radio and a node beside the group, and advertises CAP_REPEATER. With PROP_MAC_REPEATER_ENABLED set, its node is a bridge between the group and its radio, as Nodes With a LoRa Radio describes; without it, the node sends only its own traffic and its host’s to the group.
  • A virtual radio has no LoRa radio: the group is its PHY, and PROP_PHY_ENABLED turns it on and off.

Configuration belongs to the device domain and is independent of the attached host. Joining the group does not enable Wi-Fi, change IP configuration, or change PROP_MAC_REPEATER_ENABLED.

Capabilities

CAP_LAN_BEARER (63) requires no other capability. It requires PROP_LAN_BEARER_GROUP and PROP_LAN_BEARER_STATE. A forwarding device also requires PROP_LAN_BEARER_ENABLED and PROP_LAN_BEARER_INGRESS_LIMIT, and, with CAP_STATS, the five counters; a virtual radio has none of these.

PROP_LAN_BEARER_ENABLED

Property 4992; Get/Set; BOOL. Defaults to false. A write changes the desired state immediately; it does not wait for the device to join the group.

PROP_LAN_BEARER_GROUP

Property 4993; Get/Set; six octets: an IPv4 address in network order, followed by a UINT16 port. Defaults to 239.255.134.121 port 15056. An address in 224.0.0.0/4 is a multicast group; any other is the peer of a point-to-point link. A write of an empty value returns it to the default; a read always reports the group in effect. A write of 0.0.0.0, 255.255.255.255, a zero port, or any other length is refused with STATUS_INVALID_ARGUMENT.

PROP_LAN_BEARER_INGRESS_LIMIT

Property 4994; Get/Set; UINT16. The most group receptions per minute the device hands to its node; see Ingress Limit. Defaults to 120. A write of an empty value returns it to the default; a read always reports the limit in effect. Zero is invalid. The device MAY admit a short burst above the rate after a quiet interval.

PROP_LAN_BEARER_STATE

Property 4995; Get/Is; UINT8. Read-only. Changes are announced to the attached host.

ValueMeaning
0Disabled: membership in the group, or a virtual radio’s PHY, is off
1Active: joined to the group and carrying frames
2No network: enabled, with no IPv4 network to join the group on

While the state is not Active, nothing is sent to the group.

Counters

On a forwarding device under CAP_STATS, with the semantics of the other statistics: cumulative UINT32_LE values, reset by writing four zero octets.

PropertyNameCounts
4845PROP_STAT_LAN_RXDatagrams handed to the node
4846PROP_STAT_LAN_RX_LIMITEDDatagrams dropped by the ingress limit, or because the node had not yet taken earlier ones
4847PROP_STAT_LAN_RX_MALFORMEDDatagrams dropped as empty, oversized, or of no recognized protocol
4848PROP_STAT_LAN_EXPORTEDTransmitted frames queued for the group
4849PROP_STAT_LAN_EXPORT_DROPPEDQueued frames discarded before they could be sent

Persistence and lifecycle

The configuration properties a device has are saved by CMD_SAVE in the device snapshot. A group or ingress limit at its default is saved as the default, not as its value, so that the device follows the default of whatever firmware it later runs. CMD_CLEAR erases saved configuration without changing live configuration; CMD_RST follows the normal saved-state restore rules. Attach and detach do not change these properties or interrupt membership in the group. Authorized mesh administrators may write them through the existing device-management binding.

Changing the group leaves the old one and joins the new one. Losing the network discards frames waiting to be sent, and the device rejoins the group when the network returns.

Virtual Radios

A virtual radio’s physical layer is the group rather than a LoRa radio: what it transmits is sent to the group, and what it receives is every datagram from the group, delivered to its host with no measurements. Its host is a node in the group.

A virtual radio uses no identifiers of its own. It declares CAP_LAN_BEARER and the host-side capabilities a companion radio does—the raw stream, host filters and keys, auto-acknowledgment, the inbound queue, and saved state—and none of the repeater, Wi-Fi, or hardware ones, since it has no node. It needs no device identity: what it sends to the group is its host’s. Its radio properties describe a LoRa channel it does not itself transmit on; their values set the frame time the host’s MAC times against.

ULCP: I2C Bus Access

A host drives the device’s I2C peripherals directly: one transaction at a time, addressed to a bus the device names and a peripheral the host chooses, with the read data coming back in the reply. The device’s own drivers keep running; the host’s transactions are interleaved with theirs on the same bus.

This is a diagnostic and bring-up interface. What is on the bus, what a register means, and what writing it does are the peripheral’s business, and the device passes none of that judgment along. A host that writes to the charger has written to the charger.

Nothing here is state. The two properties are constant descriptions of the hardware, neither command changes anything the device remembers, and none of it is part of a saved snapshot.

Capabilities

CodeNameRequiresGrants
60CAP_I2C—PROP_I2C_BUSES, PROP_I2C_DEVICES, CMD_I2C_TRANSFER, CMD_I2C_SCAN, CMD_I2C_RESULT

A device advertises CAP_I2C when it offers at least one bus. Without it the two commands are STATUS_UNIMPLEMENTED and the two properties STATUS_PROP_NOT_FOUND, as for any capability the device lacks.

Properties

Identifiers 4960 through 4975 are allocated to this subsystem.

PROP 4960: PROP_I2C_BUSES

  • Type: Multiple-Value, Constant
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_I2C
  • Item Form: structure below
  • Post-Reset Value: Unchanged

The buses a host may drive. Each item:

+-----+-----------+----------+---------+------------+
| BUS | SPEED_KHZ | MAX_DATA | MAX_OPS |  NAME ...  |
+-----+-----------+----------+---------+------------+
  1 B     2 B         2 B       1 B      remainder

Figure: Bus item format

BUS is the number CMD_I2C_TRANSFER and CMD_I2C_SCAN name the bus by. SPEED_KHZ (UINT16, little-endian) is the clock the device drives it at. MAX_DATA (UINT16, little-endian) is the largest sum of octets written and octets read the device accepts in one transfer; MAX_OPS is the largest operation count. NAME is UTF-8 display text naming the controller and its pins, such as I2C0 SDA GPIO3 SCL GPIO2, and is at most 64 octets.

PROP 4961: PROP_I2C_DEVICES

  • Type: Multiple-Value, Constant
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_I2C
  • Item Form: structure below
  • Post-Reset Value: Rebuilt from startup identification where supported

The peripherals the device’s own firmware knows to be on its buses. Each item:

+-----+------+------------+
| BUS | ADDR |  NAME ...  |
+-----+------+------------+
  1 B   1 B    remainder

Figure: Peripheral item format

BUS is a bus from PROP_I2C_BUSES, ADDR the peripheral’s 7-bit address, and NAME UTF-8 display text naming the part, at most 64 octets. This is the firmware’s cached peripheral inventory. A board may build it from startup identification and successful driver initialization; the result remains fixed until reboot. Other boards use a fixed table of known peripherals. Reading this property does not scan or probe the bus. A host may query this table explicitly for peripheral information. A device with CAP_I2C and no such knowledge reports an empty table.

A CMD_PROP_SET, CMD_PROP_INSERT, or CMD_PROP_REMOVE of either property is STATUS_INVALID_ARGUMENT.

Commands

CMD 25: (Host -> Device) CMD_I2C_TRANSFER

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |CMD_I2C_TRANSFER|      BUS      |      ADDR     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      OP ...
+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_I2C_TRANSFER

One I2C transaction with the peripheral at 7-bit address ADDR on bus BUS. The payload after the address is a sequence of one or more operations:

+------+-------+----------------------+
| KIND |  LEN  |  DATA (write only)   |
+------+-------+----------------------+
  1 B    PUI       LEN octets

Figure: Transfer operation format

KIND is 0 for a write and 1 for a read. LEN is the number of octets the operation moves: a write carries them in DATA; a read carries nothing and the octets come back in the reply.

The transaction is performed as one bus sequence: START, the address, the operations in order, and one STOP. A change of direction between consecutive operations is a repeated START with the address resent; consecutive operations of the same direction continue without one, so a write followed by a write is one longer write on the wire, and a read followed by a read is one longer read. This is the transaction model of common HAL interfaces, and a register read is a write of the register address followed by a read.

Only 7-bit addressing is defined. The reserved address ranges (0x00–0x07 and 0x78–0x7F) are not policed: what a general call or a device-ID probe does is the host’s affair.

On success the device answers with CMD_I2C_RESULT carrying the command’s TID, whose payload is the read data of every read operation, concatenated in operation order. A transfer with no read operation is answered with an empty CMD_I2C_RESULT.

Failures are reported through PROP_LAST_STATUS with the command’s TID:

ConditionStatus
The payload is truncated or does not parse as the structure aboveSTATUS_PARSE_ERROR
ADDR exceeds 0x7F; an unknown KIND; an operation with LEN zero; no operations; more octets than MAX_DATA; more operations than MAX_OPSSTATUS_INVALID_ARGUMENT
BUS is not an item of PROP_I2C_BUSESSTATUS_ITEM_NOT_FOUND
The CMD_I2C_RESULT would not fit the binding’s replySTATUS_NOMEM
The bus is not operable now (its rail is off)STATUS_INVALID_STATE
The bus could not be acquired within the device’s bound, or the peripheral is held by one of the device’s own procedures (Sharing the Bus)STATUS_BUSY
No peripheral acknowledged ADDRSTATUS_NO_DEVICE
The peripheral did not acknowledge a data octetSTATUS_NACK
The bus itself failed: arbitration lost, a bus error, a stuck line, or the transaction outlasting the device’s deadlineSTATUS_BUS_ERROR
The device lacks CAP_I2CSTATUS_UNIMPLEMENTED

Everything above the bus—parsing, the limits, the bus number, and whether the reply could be carried—is checked before the bus is touched, so a refused request has done nothing. A failure on the bus carries no partial read data, but operations before the failing one have already acted on the wire: a STATUS_NACK on the third operation means the first two completed.

Telling an unacknowledged address from an unacknowledged data octet is something a controller either reports or estimates, and some cannot do it at all for a transfer that begins with a write. Where the device cannot tell, it reports STATUS_NACK. A host therefore reads STATUS_NO_DEVICE as “nothing is there” but must not read STATUS_NACK as “something is there”; whether an address answers at all is CMD_I2C_SCAN’s question.

A device performs the whole transaction under a deadline of its own choosing, and a transaction that does not complete within it is STATUS_BUS_ERROR, after which the device MAY attempt bus recovery. A zero-length operation is refused rather than performed because a bare address frame is a probe, which is what CMD_I2C_SCAN is for.

CMD 27: (Host -> Device) CMD_I2C_SCAN

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |  CMD_I2C_SCAN |      BUS      |     FIRST     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     LAST      |
+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_I2C_SCAN

Probe every 7-bit address from FIRST through LAST inclusive on bus BUS and report which acknowledged. FIRST and LAST are optional together; absent, the range is 0x08 through 0x77, the addresses outside the reserved ranges. FIRST greater than LAST, or LAST greater than 0x7F, is STATUS_INVALID_ARGUMENT; a payload of any other length is STATUS_PARSE_ERROR.

Each probe is a one-octet read, or a bare address frame on a controller that can send one. A probe is never a write. The reply is a CMD_I2C_RESULT carrying the command’s TID, whose payload is the acknowledged addresses in ascending order, one octet each; nothing acknowledged is an empty payload.

A scan is a hint, not an inventory. A peripheral that does not acknowledge reads goes unreported, and on some peripherals a one-octet read is a real read: it can pop a FIFO or clear a latched flag. The bus, bus-number, and reply-size refusals are as for CMD_I2C_TRANSFER, with STATUS_NOMEM judged against the whole range; a bus failure during the scan is STATUS_BUS_ERROR with no partial list.

CMD 26: (Device -> Host) CMD_I2C_RESULT

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+
|1 0| RES | TID | CMD_I2C_RESULT|   DATA ...  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+

Figure: Structure of CMD_I2C_RESULT

The successful answer to CMD_I2C_TRANSFER or CMD_I2C_SCAN, carrying the request’s TID. DATA is what the request defines: concatenated read data for a transfer, acknowledged addresses for a scan. It is never sent unsolicited, and a CMD_I2C_RESULT received by a device is STATUS_INVALID_COMMAND like any other Device→Host command.

A CMD_I2C_RESULT is one frame and is never continued: a device refuses, with STATUS_NOMEM before touching the bus, any request whose result would not fit the reply its binding can carry. On a local binding that bound is the frame size; on the Node Management binding it is the payload budget described below.

Sharing the Bus

The device serializes transactions, not procedures. Its own drivers keep using the bus between a host’s commands, and separate commands form no exclusive session, so a host procedure of several steps—an unseal, a read, a reseal—can be interleaved with the device’s own traffic to the same peripheral. A host that needs a sequence performed without interleaving puts it in one CMD_I2C_TRANSFER, which is what the operation list is for.

The same holds in the other direction, and a device MAY protect its own multi-transaction procedures: while one runs, a transfer addressed to the peripheral it holds, or a scan whose range covers that address, is STATUS_BUSY. The refusal covers the whole scan rather than silently omitting the address, so the host is told to try again instead of being handed a wrong inventory. A device MUST NOT hold the bus against its own drivers for the duration of a host’s procedure; the host retries STATUS_BUSY after a moment.

A device bounds how long a transfer waits for the bus, and a wait that exceeds the bound is STATUS_BUSY. The bound is the device’s; a host allows for a transaction to take longer to answer than a property read.

Over the Node Management Binding

Both commands are available to a listed administrator over the Node Management binding, reads and writes alike, and CMD_I2C_RESULT is one of the frames a Response carries.

The Response is one payload with no continuation, so the reply budget is what remains of a payload after the envelope: a transfer whose reads, or a scan whose range, would exceed it is STATUS_NOMEM. An administrator reads in pieces; the budget is well over a hundred octets, and a peripheral’s register file rarely needs more at once.

The binding answers a retransmitted request from its retained response rather than executing it again, for as long as that entry exists. Entries can be evicted and do not survive a reset of the device. Two consequences follow for writes. A write whose effect resets the node—a power-management register, say—is never confirmed, because the node that would confirm it is gone. And a write whose Response was lost leaves the administrator unable to tell whether it acted: a retransmission is answered from the retained entry if it still exists, and executed afresh if it does not. An administrator that must know reads the register back rather than retransmitting the write, and treats a write to a register that can reset the node as one-shot.

Security Considerations

This is raw register access to whatever is on the bus: on a typical board the charger, the power-management IC, the gauge, the real-time clock, and the sensors. A host can misconfigure the charger, cut a rail, or corrupt a gauge’s learned state, and the device does not stand in the way.

On a local binding the host already has the device in hand. Over Node Management the same access is granted to every listed administrator, who can already factory-reset the node and rewrite its identity; the bus adds the ability to damage the hardware’s configuration rather than just the node’s. An operator lists administrators accordingly. PROP_I2C_DEVICES provides peripheral information on explicit request; scans and transfers do not require a preliminary query of this table.

ULCP: Minimum Requirements

This chapter is the conformance statement for ULCP. A device that meets the requirements below is a ULCP device; everything past them is optional and is discovered through PROP_CAPS or a command’s defined refusal.

There are no protocol levels, tiers, or profiles. A host that wants a plain frame pipe and a host that wants a fully provisioned companion radio speak the same protocol to the same devices and differ only in which capabilities they look for. A device implements the subsystems its hardware and purpose call for and advertises exactly those.

Framing

A device MUST implement the frame format in full: the header with its FLG, RESERVED, and TID fields, the packed unsigned integer encoding for command, property, and stream identifiers, and the transport’s framing (HDLC-Lite on serial links, the GATT frame transport over BLE).

The TID discipline is normative in both directions: a device MUST NOT send a frame with a non-zero TID that is not a response to a frame it recently received with that TID, and all unsolicited commands MUST use TID zero.

Commands

IdMnemonicRequired
0CMD_NOPAlways
1CMD_RSTAlways
2CMD_PROP_GETAlways
3CMD_PROP_SETAlways
6CMD_PROP_ISAlways
9CMD_STR_SENDAlways
10CMD_STR_RECVAlways
17CMD_DFUAlways; unsupported methods return STATUS_UNIMPLEMENTED

CMD_PROP_INSERT, CMD_PROP_REMOVE, and their notifications belong to the base grammar rather than to any capability. A device that defines no mutable multi-value property has nothing to apply them to and rejects them under the ordinary property rules. CMD_QUEUE_DRAIN, CMD_SAVE, and CMD_RESTORE belong to their subsystems’ capabilities and MUST fail with STATUS_UNIMPLEMENTED when the capability is not advertised, as MUST CMD_REBOOT, CMD_DFU, and CMD_FACTORY_RESET without CAP_REBOOT and CMD_ANNOUNCE without CAP_ADVERT; CMD_CLEAR is available regardless of capabilities (see Saved State).

DFU implementations MUST validate the optional mode, require a nonzero direct TID, finish fallible preparation before success, and wait for the matching response’s transmission before handing off to the bootloader. Transmission failure or timeout MUST cancel the handoff and invalidate retained success. Tests must cover stale completions, administrative retries, and replay-counter persistence failures; a build alone does not qualify a bootloader’s supported DFU methods. See CMD_DFU.

Properties

IdMnemonicRequired
0PROP_LAST_STATUSAlways
1PROP_PROTOCOL_VERSIONAlways
2PROP_DEV_VERSIONAlways
3PROP_INTERFACE_TYPEAlways
5PROP_CAPSAlways
32PROP_PHY_ENABLEDAlways, Get and Set
35PROP_PHY_FREQAlways
37PROP_PHY_TX_POWERAlways
38PROP_PHY_RSSIAlways
42PROP_PHY_MTUAlways
44PROP_PHY_T_FRAMEAlways (new implementations)
113STR_PHY_RAWAlways

PROP_DEV_MODEL (4) and PROP_UPTIME (6) are the two properties that are neither always required nor capability-gated. Firmware built for a specific board SHOULD implement the first, and a device with a monotonic clock SHOULD implement the second; anything else omits them. A host discovers both by asking.

Every other property is gated by a capability. A device that does not advertise the capability does not implement the property, and rejects it with STATUS_PROP_NOT_FOUND or STATUS_UNIMPLEMENTED.

Status and Reset Reporting

A device MUST implement PROP_LAST_STATUS as the failure channel for every command, using the most specific applicable status code, and MUST emit a reset code asynchronously after every reset. A device that cannot determine the cause reports STATUS_RESET_UNKNOWN rather than omitting the notification.

Optional Subsystems

Everything else in this specification is a capability. Each grants the commands and properties defined in its chapter, and a device MUST NOT advertise a capability without also advertising the capabilities it requires.

SubsystemCapabilities
Radio Control beyond the required propertiesCAP_PHY_LORA, CAP_PHY_DUTY_LIMIT, CAP_STATS
Device DomainCAP_DEV_IDENTITY, CAP_DEV_NAME, CAP_BATTERY, CAP_REPEATER, CAP_IDENT, CAP_ALERT, CAP_TIME, CAP_GNSS, CAP_ADVERT, CAP_ILLUMINANCE, CAP_DISPLAY_MOTION_WAKE, CAP_DISPLAY, CAP_KEYBOARD
Saved StateCAP_SAVE
Tethered Host ServicesCAP_HOST_FILTER, CAP_HOST_KEYS, CAP_HOST_RX_QUEUE, CAP_HOST_AUTO_ACK, CAP_MAC_BACKHAUL
Wi-FiCAP_WIFI_SCAN, CAP_WIFI, CAP_WIFI_AP
IP ConnectivityCAP_IPV4, CAP_IPV6
Bridge ClientCAP_BRIDGE_CLIENT
IP MulticastCAP_LAN_BEARER
I2C Bus AccessCAP_I2C

A device advertising none of them is a transparent radio: it configures its PHY, transmits what it is given, and delivers everything it hears.

Requirements on Hosts

A conforming host:

  • MUST tolerate unsolicited CMD_PROP_IS, CMD_PROP_INSERTED, and CMD_PROP_REMOVED notifications at any time while attached, and update its view of the affected property accordingly. Device state changes for reasons the host did not initiate, and publication of the new authoritative value is how the protocol reports it.
  • MUST take the value in a CMD_PROP_IS as the property’s value, and MUST NOT treat one that differs from what it wrote as an error. A write is refused by a PROP_LAST_STATUS carrying the failure and by nothing else; anything a device reports as a property value is what that property is, whether or not it is what was asked for—see PROP_PHY_TX_POWER, which a device clamps to what its radio can reach. A host that shows the value to a user shows the reported one.
  • MUST NOT treat a failed capability-gated property read as a failed attach. A device advertising a capability implements its properties, so a refusal is a device fault—but what is unknown is the setting, not the device. A host finishes the rest of the read, presents the affected setting as unavailable rather than as a default, and omits it from what it writes.
  • MUST NOT assume that a reset implies documented factory defaults. On a device holding a saved snapshot the post-reset value of every saved property is its saved value; a host fetches or explicitly sets what it depends on.
  • MUST establish its complete host domain on every tethered attach, if it uses host services at all, rather than reasoning about what the device already holds.
  • MUST NOT write host-domain properties when it is merely administering a device rather than being that device’s host—see Two Kinds of Attach.
  • SHOULD follow the post-attach procedure in Attach, Detach, and Synchronization.

Deployment Shapes

The capability sets that correspond to the familiar deployments, as a reader’s aid rather than a normative classification:

DeploymentTypical capabilities
Transparent radioCAP_WRITABLE_RAW_STREAM, CAP_PHY_LORA, CAP_PHY_DUTY_LIMIT
Companion radioThe above, plus CAP_HOST_FILTER, CAP_HOST_KEYS, CAP_HOST_RX_QUEUE, CAP_HOST_AUTO_ACK
Commissioned repeaterThe above, plus CAP_DEV_IDENTITY, CAP_SAVE, CAP_REPEATER, CAP_IDENT

The same firmware ordinarily advertises all of them: which deployment a device is doing is a matter of what its operator provisioned, not of what it can do.

ULCP: Command and Property Index

Every numeric identifier the protocol defines, and where it is specified.

Commands

IdMnemonicDirGated by
0CMD_NOPHost->Device—
1CMD_RSTHost->Device—
2CMD_PROP_GETHost->Device—
3CMD_PROP_SETHost->Device—
4CMD_PROP_INSERTHost->Device—
5CMD_PROP_REMOVEHost->Device—
6CMD_PROP_ISDevice->Host—
7CMD_PROP_INSERTEDDevice->Host—
8CMD_PROP_REMOVEDDevice->Host—
9CMD_STR_SENDHost->Device—
10CMD_STR_RECVDevice->Host—
11CMD_QUEUE_DRAINHost->DeviceCAP_HOST_RX_QUEUE
12CMD_SAVEHost->DeviceCAP_SAVE
13CMD_CLEARHost->Device—
14CMD_RESTOREHost->DeviceCAP_SAVE
15CMD_FACTORY_RESETHost->DeviceCAP_REBOOT
16CMD_REBOOTHost->DeviceCAP_REBOOT
17CMD_DFUHost->DeviceCAP_REBOOT
19CMD_ANNOUNCEHost->DeviceCAP_ADVERT
21CMD_PROP_MULTI_GETHost->DeviceCAP_CMD_MULTI
22CMD_PROP_MULTI_SETHost->DeviceCAP_CMD_MULTI
23CMD_PROP_AREDevice->HostCAP_CMD_MULTI
24CMD_SESSION_RESETDevice->Host—
25CMD_I2C_TRANSFERHost->DeviceCAP_I2C
26CMD_I2C_RESULTDevice->HostCAP_I2C
27CMD_I2C_SCANHost->DeviceCAP_I2C

Command identifiers are 7-bit; 18, 20, and 28–127 are unassigned.

Properties and Streams

Identifiers are allocated by state class; see Property Allocation.

IdMnemonicCommandsGated by
0PROP_LAST_STATUSGet, Is—
1PROP_PROTOCOL_VERSIONGet—
2PROP_DEV_VERSIONGet—
3PROP_INTERFACE_TYPEGet—
4PROP_DEV_MODELGet—
5PROP_CAPSGet—
6PROP_UPTIMEGet—
32PROP_PHY_ENABLEDGet, Set—
35PROP_PHY_FREQGet, Set—
37PROP_PHY_TX_POWERGet, Set—
38PROP_PHY_RSSIGet—
39PROP_PHY_LORA_BWGet, SetCAP_PHY_LORA
40PROP_PHY_LORA_SFGet, SetCAP_PHY_LORA
41PROP_PHY_LORA_CRGet, SetCAP_PHY_LORA
42PROP_PHY_MTUGet—
43PROP_PHY_LORA_SWGet, SetCAP_PHY_LORA
44PROP_PHY_T_FRAMEGet—
48PROP_MAC_PROMISCUOUSGet, SetCAP_HOST_FILTER
49PROP_SAVEDGetCAP_SAVE
50PROP_MAC_BACKHAULGet, SetCAP_MAC_BACKHAUL
64PROP_DEV_KEYGetCAP_DEV_IDENTITY
65PROP_DEV_PRIVATE_KEYSetCAP_DEV_IDENTITY
66PROP_DEV_CHANNEL_KEYSGet, Set, Insert, RemoveCAP_DEV_IDENTITY
67PROP_DEV_PEERSGet, Set, Insert, RemoveCAP_DEV_IDENTITY
68PROP_DEV_NAMEGet, SetCAP_DEV_NAME
69PROP_BATTERYGet, IsCAP_BATTERY
70PROP_MAC_REPEATER_ENABLEDGet, SetCAP_REPEATER
71PROP_IDENTGetCAP_IDENT
72PROP_IDENT_ROLEGet, SetCAP_IDENT
73PROP_IDENT_MOBILEGet, SetCAP_IDENT
74PROP_MAC_REPEATER_REGIONSGet, SetCAP_REPEATER
75PROP_MAC_REPEATER_DEFAULT_REGIONGet, SetCAP_REPEATER
76PROP_MAC_REPEATER_MIN_RSSIGet, SetCAP_REPEATER
77PROP_MAC_REPEATER_MIN_SNRGet, SetCAP_REPEATER
78PROP_DEV_DISCOVERABLEGet, SetCAP_DEV_IDENTITY
79PROP_ALERTGet, Set, IsCAP_ALERT
80PROP_ADVERT_INTERVALGet, SetCAP_ADVERT
81PROP_BEACON_INTERVALGet, SetCAP_ADVERT
82PROP_STARTUP_BEACONGet, SetCAP_ADVERT
83PROP_IDENT_LOCATIONGet, SetCAP_IDENT
84PROP_IDENT_ALTITUDEGet, SetCAP_IDENT
85PROP_IDENT_CONTACTGetCAP_IDENT
88PROP_GNSS_ENABLEDGet, SetCAP_GNSS
89PROP_GNSS_LOCATIONGet, IsCAP_GNSS
90PROP_GNSS_ALTITUDEGetCAP_GNSS
91PROP_GNSS_FIXGet, IsCAP_GNSS
92PROP_GNSS_PRECISIONGetCAP_GNSS
93PROP_GNSS_SATELLITESGetCAP_GNSS
94PROP_ILLUMINANCEGetCAP_ILLUMINANCE
95PROP_TEMPERATURESGetCAP_TEMPERATURE
96PROP_HOST_KEYGet, SetCAP_HOST_FILTER
97PROP_HOST_CHANNEL_KEYSGet, Set, Insert, RemoveCAP_HOST_KEYS
98PROP_HOST_PEER_KEYSGet, Set, Insert, RemoveCAP_HOST_KEYS
99PROP_HOST_RX_FILTERSGet, Set, Insert, RemoveCAP_HOST_FILTER
100PROP_HOST_AUTO_ACKGet, SetCAP_HOST_AUTO_ACK
101PROP_HOST_RX_QUEUE_COUNTGetCAP_HOST_RX_QUEUE
102PROP_HOST_RX_QUEUE_CAPACITYGet, SetCAP_HOST_RX_QUEUE
103PROP_HOST_RX_QUEUE_DROPPEDGetCAP_HOST_RX_QUEUE
104PROP_HOST_MUTED_CHANNELSGet, Set, Insert, RemoveCAP_HOST_RX_QUEUE
105PROP_HOST_MUTED_PEERSGet, Set, Insert, RemoveCAP_HOST_RX_QUEUE
113STR_PHY_RAWSend, Recv—
4820PROP_PHY_DUTY_NOWGetCAP_PHY_DUTY_LIMIT
4822PROP_PHY_DUTY_LIMITGet, SetCAP_PHY_DUTY_LIMIT
4832PROP_STAT_TX_PACKETSGet, SetCAP_STATS
4833PROP_STAT_TX_CHANNEL_BUSYGet, SetCAP_STATS
4834PROP_STAT_RX_PACKETSGet, SetCAP_STATS
4835PROP_STAT_RX_BAD_CRCGet, SetCAP_STATS
4836PROP_STAT_RX_NON_UMSHGet, SetCAP_STATS
4837PROP_STAT_RX_ACCEPTEDGet, SetCAP_STATS, CAP_REPEATER
4838PROP_STAT_FORWARDEDGet, SetCAP_STATS, CAP_REPEATER
4839PROP_STAT_FORWARD_DROPPEDGet, SetCAP_STATS, CAP_REPEATER
4840PROP_STAT_FORWARD_CANCELLEDGet, SetCAP_STATS, CAP_REPEATER
4841PROP_STAT_MESHCORE_SEENGet, SetCAP_STATS, CAP_MESHCORE_FORWARDER
4842PROP_STAT_MESHCORE_FORWARDED_ROUTEDGet, SetCAP_STATS, CAP_MESHCORE_FORWARDER
4843PROP_STAT_MESHCORE_FORWARDED_FLOODGet, SetCAP_STATS, CAP_MESHCORE_FORWARDER
4844PROP_STAT_MESHCORE_DECLINEDGet, SetCAP_STATS, CAP_MESHCORE_FORWARDER
4845PROP_STAT_LAN_RXGet, SetCAP_STATS, CAP_LAN_BEARER, CAP_REPEATER
4846PROP_STAT_LAN_RX_LIMITEDGet, SetCAP_STATS, CAP_LAN_BEARER, CAP_REPEATER
4847PROP_STAT_LAN_RX_MALFORMEDGet, SetCAP_STATS, CAP_LAN_BEARER, CAP_REPEATER
4848PROP_STAT_LAN_EXPORTEDGet, SetCAP_STATS, CAP_LAN_BEARER, CAP_REPEATER
4849PROP_STAT_LAN_EXPORT_DROPPEDGet, SetCAP_STATS, CAP_LAN_BEARER, CAP_REPEATER
4850PROP_STAT_RX_OWN_HINTGet, SetCAP_STATS, CAP_REPEATER
4851PROP_STAT_RADIO_RESETSGet, SetCAP_STATS
4864PROP_BLE_PAIRING_PINSetBLE transport
4865PROP_DEV_ADMINSGet, Set, Insert, RemoveCAP_ADMIN
4866PROP_TIMEGet, Set, IsCAP_TIME
4868PROP_GNSS_IDENT_UPDATEGet, SetCAP_GNSS
4869PROP_GNSS_IDENT_PRECISIONGet, SetCAP_GNSS
4870PROP_GNSS_TIME_TRUSTGet, Set, IsCAP_GNSS
4871PROP_BLE_ENABLEDGet, Set, IsCAP_BLE
4872PROP_BLE_BOND_COUNTGet, Set, IsCAP_BLE
4873PROP_BLE_LINKGet, IsCAP_BLE
4874PROP_BLE_PAIRINGGet, Set, IsCAP_BLE
4875PROP_DISPLAY_MOTION_WAKE_ENABLEDGet, Set, IsCAP_DISPLAY_MOTION_WAKE
4876PROP_TEMPERATURE_NAMESGetCAP_TEMPERATURE
4877PROP_DISPLAY_BRIGHTNESSGet, Set, IsCAP_DISPLAY
4878PROP_KEYBOARD_LIGHTGet, Set, IsCAP_KEYBOARD
4879PROP_KEYBOARD_AIDSGet, Set, IsCAP_KEYBOARD
4880PROP_WIFI_ENABLEDGet, Set, IsCAP_WIFI
4881PROP_WIFI_NETWORKSGet, Set, Insert, RemoveCAP_WIFI
4882PROP_WIFI_NETWORKGet, Set, IsCAP_WIFI
4883PROP_WIFI_SCANNINGGet, Set, IsCAP_WIFI_SCAN
4884PROP_WIFI_SCAN_RESULTSGet, Is, InsertedCAP_WIFI_SCAN
4885PROP_WIFI_LINKGet, IsCAP_WIFI
4886PROP_WIFI_RSSIGetCAP_WIFI
4887PROP_WIFI_MACGetCAP_WIFI
4888PROP_TZ_RULEGet, Set, IsCAP_TIME
4889PROP_TIME_24HGet, Set, IsCAP_TIME
4890PROP_DISPLAY_TIMEOUTGet, Set, IsCAP_DISPLAY
4891PROP_SOUND_ENABLEDGet, Set, IsCAP_SOUND
4892PROP_SOUND_VOLUMEGet, Set, IsCAP_SOUND
4893PROP_SETUP_COMPLETEGet, Set, IsCAP_KEYBOARD
4894PROP_TEST_MESSAGE_ALERTSGet, Set, Is—
4896PROP_IPV4_STATEGet, IsCAP_IPV4
4897PROP_IPV4_CONFIGGet, SetCAP_IPV4
4898PROP_IPV4_ADDRESSGet, IsCAP_IPV4
4899PROP_IPV6_STATEGet, IsCAP_IPV6
4900PROP_IPV6_CONFIGGet, SetCAP_IPV6
4901PROP_IPV6_ADDRESSESGet, IsCAP_IPV6
4902PROP_IP_DNSGet, Set, Insert, RemoveCAP_IPV4 or CAP_IPV6
4903PROP_IP_RESOLVERSGet, IsCAP_IPV4 or CAP_IPV6
4912PROP_WIFI_AP_ENABLEDGet, Set, IsCAP_WIFI_AP
4913PROP_WIFI_AP_CONFIGGet, SetCAP_WIFI_AP
4914PROP_WIFI_AP_STATEGet, IsCAP_WIFI_AP
4915PROP_WIFI_AP_CLIENTSGet, Is, Inserted, RemovedCAP_WIFI_AP
4928PROP_BRIDGE_ENABLEDGet, SetCAP_BRIDGE_CLIENT
4929PROP_BRIDGE_HOSTGet, SetCAP_BRIDGE_CLIENT
4930PROP_BRIDGE_PORTGet, SetCAP_BRIDGE_CLIENT
4931PROP_BRIDGE_SERVER_KEYGet, SetCAP_BRIDGE_CLIENT
4932PROP_BRIDGE_LINKGet, IsCAP_BRIDGE_CLIENT
4944PROP_BATTERY_CURRENTGet, IsOptional with CAP_BATTERY
4945PROP_BATTERY_REMAINING_CAPACITYGet, IsOptional with CAP_BATTERY
4946PROP_BATTERY_FULL_CAPACITYGet, IsOptional with CAP_BATTERY
4947PROP_BATTERY_DESIGN_CAPACITYGet, IsOptional with CAP_BATTERY
4948PROP_BATTERY_EXT_POWER_PRESENTGet, IsOptional with CAP_BATTERY
4949PROP_BATTERY_PRESENTGet, IsOptional with CAP_BATTERY
4950PROP_BATTERY_GAUGE_FULLGet, IsOptional with CAP_BATTERY
4951PROP_BATTERY_GAUGE_INITIALIZEDGet, IsOptional with CAP_BATTERY
4952PROP_BATTERY_GAUGE_SMOOTHINGGet, IsOptional with CAP_BATTERY
4953PROP_BATTERY_CHARGE_VOLTAGE_REQUESTGet, IsOptional with CAP_BATTERY
4954PROP_BATTERY_GAUGE_FORMATGet, IsOptional with CAP_BATTERY
4955PROP_BATTERY_GAUGE_STATUSGet, IsOptional with CAP_BATTERY
4956PROP_BATTERY_GAUGE_OPERATION_STATUSGet, IsOptional with CAP_BATTERY
4957PROP_BATTERY_GAUGE_CONFIGGet, IsOptional with CAP_BATTERY; explicit configuration inspection
4958PROP_BATTERY_GAUGE_TELEMETRYGet, IsOptional with CAP_BATTERY; live counters, temperatures, and estimates
4960PROP_I2C_BUSESGet, IsCAP_I2C
4961PROP_I2C_DEVICESGet, IsCAP_I2C
4976PROP_MESHCORE_FORWARDING_ENABLEDGet, SetCAP_MESHCORE_FORWARDER
4977PROP_MESHCORE_REGION_POLICYGet, SetCAP_MESHCORE_FORWARDER
4978PROP_MESHCORE_REGIONSGet, Set, Insert, RemoveCAP_MESHCORE_FORWARDER
4992PROP_LAN_BEARER_ENABLEDGet, SetCAP_LAN_BEARER, CAP_REPEATER
4993PROP_LAN_BEARER_GROUPGet, SetCAP_LAN_BEARER
4994PROP_LAN_BEARER_INGRESS_LIMITGet, SetCAP_LAN_BEARER, CAP_REPEATER
4995PROP_LAN_BEARER_STATEGet, IsCAP_LAN_BEARER

Capabilities

Advertised through PROP_CAPS; the allocation is in Capabilities.

CodeNameDefined in
8CAP_WRITABLE_RAW_STREAMFrame Transport
16CAP_PHY_DUTY_LIMITRadio Control
32CAP_HOST_FILTERTethered Host Services
33CAP_HOST_RX_QUEUETethered Host Services
34CAP_HOST_KEYSTethered Host Services
35CAP_HOST_AUTO_ACKTethered Host Services
36CAP_SAVESaved State
37CAP_DEV_IDENTITYDevice Domain
38CAP_DEV_NAMEDevice Domain
39CAP_BATTERYDevice Domain
40CAP_REPEATERDevice Domain
41CAP_IDENTDevice Domain
42CAP_ALERTDevice Domain
43CAP_ADMINNode Management
44CAP_TIMEDevice Domain
45CAP_GNSSDevice Domain
46CAP_ADVERTDevice Domain
47CAP_ILLUMINANCEDevice Domain
48CAP_MAC_BACKHAULTethered Host Services
49CAP_CMD_MULTIFraming and Common Semantics
50CAP_BLEBLE Binding
51CAP_REBOOTFraming and Common Semantics
52CAP_STATSRadio Control
53CAP_WIFI_SCANWi-Fi
54CAP_WIFIWi-Fi
55CAP_IPV4IP Connectivity
56CAP_IPV6IP Connectivity
57CAP_WIFI_APWi-Fi
58CAP_BRIDGE_CLIENTBridge Client
59CAP_DISPLAY_MOTION_WAKEDevice Services
60CAP_I2CI2C Bus Access
61CAP_TEMPERATUREDevice Domain
62CAP_MESHCORE_FORWARDERDevice Domain
63CAP_LAN_BEARERIP Multicast
64CAP_DISPLAYDevice Domain
65CAP_KEYBOARDDevice Domain
66CAP_SOUNDDevice Domain
515CAP_PHY_LORARadio Control

Status Codes

Defined in Status Codes.

IdNameIdName
0STATUS_OK13STATUS_PROP_NOT_FOUND
1STATUS_FAILURE18STATUS_CCA_FAILURE
2STATUS_UNIMPLEMENTED19STATUS_ALREADY
3STATUS_INVALID_ARGUMENT20STATUS_ITEM_NOT_FOUND
4STATUS_INVALID_STATE21STATUS_CURSOR_INVALID
5STATUS_INVALID_COMMAND22STATUS_NOT_PERMITTED
7STATUS_INTERNAL_ERROR23STATUS_CHANNEL_NOT_FOUND
9STATUS_PARSE_ERROR24STATUS_NO_DEVICE
10STATUS_IN_PROGRESS25STATUS_NACK
11STATUS_NOMEM26STATUS_BUS_ERROR
12STATUS_BUSY32STATUS_DUTY_LIMIT

Reset Codes

Defined in Reset Codes; the range 112–127 is reserved for them.

IdNameIdName
112STATUS_RESET_POWER_ON117STATUS_RESET_ASSERT
113STATUS_RESET_EXTERNAL118STATUS_RESET_OTHER
114STATUS_RESET_SOFTWARE119STATUS_RESET_UNKNOWN
115STATUS_RESET_RESTORED120STATUS_RESET_WATCHDOG
116STATUS_RESET_CRASH

Enumerated Values

The value enumerations carried inside properties and stream metadata.

EnumerationValuesDefined in
PROP_SAVED0 none, 1 current, 2 fallback, 3 unreadablePROP_SAVED
Filter types0 FILTER_DEST_HINT, 1 FILTER_CHANNEL_ID, 2 FILTER_PKT_TYPEPROP_HOST_RX_FILTERS
Charge states0 discharging, 1 charging, 2 charged, 3 not chargingPROP_BATTERY
Alert states0 ALERT_NONE, 1 ALERT_LOCATEPROP_ALERT
Keyboard aidsbit 0 KEYBOARD_AID_CAPITALIZE, bit 1 KEYBOARD_AID_PERIODPROP_KEYBOARD_AIDS
Fix quality0 none, 1 two-dimensional, 2 three-dimensionalPROP_GNSS_FIX
MeshCore region policies0 open, 1 scoped only, 2 ignorePROP_MESHCORE_REGION_POLICY
Transmit flagsbit 0 TX_FLAG_NOCCA, bit 1 TX_FLAG_NODUTYSTR_PHY_RAW
Receive flagsbit 0 RX_FLAG_BUFFERED, bit 1 RX_FLAG_ACKED, bit 2 RX_FLAG_SELF_TXExtended Recv Metadata
Session reset reasons0 attached, 1 CMD_RST, 2 CMD_RESTORECMD_SESSION_RESET
Wi-Fi security modes0 WIFI_SEC_OPEN, 1 WIFI_SEC_OWE, 2 WIFI_SEC_WPA2, 3 WIFI_SEC_WPA3, 4 WIFI_SEC_WPA, 5 WIFI_SEC_WEP, 6 WIFI_SEC_WPA2_ENT, 7 WIFI_SEC_WPA3_ENT, 8 WIFI_SEC_WPA3_ENT_192PROP_WIFI_NETWORKS
Wi-Fi link states0 WIFI_LINK_DOWN, 1 WIFI_LINK_CONNECTING, 2 WIFI_LINK_UPPROP_WIFI_LINK
Wi-Fi link reasons0 WIFI_REASON_NONE, 1 WIFI_REASON_NOT_FOUND, 2 WIFI_REASON_AUTH, 3 WIFI_REASON_REJECTED, 4 WIFI_REASON_LOST, 5 WIFI_REASON_OTHERPROP_WIFI_LINK
Access point states0 WIFI_AP_DOWN, 1 WIFI_AP_UPPROP_WIFI_AP_STATE
IP family states0 IP_DISABLED, 1 IP_NO_LINK, 2 IP_WAITING, 3 IP_READY, 4 IP_CONFLICTPROP_IPV4_STATE
IP configuration methods0 IP_METHOD_DISABLED, 1 IP_METHOD_AUTO, 2 IP_METHOD_STATICPROP_IPV4_CONFIG
IPv6 address item kinds0 IPV6_ADDRESS, 1 IPV6_ROUTERPROP_IPV6_ADDRESSES

ULCP over BLE

This chapter defines the normative binding of ULCP (see Framing and Common Semantics) onto Bluetooth Low Energy. It covers the tethered case only: one host device driving its own companion radio over a BLE connection, exactly as it would over UART or USB-CDC.

Using BLE as a shared local bearer—nearby devices exchanging UMSH frames over BLE as peers, or reaching the UMSH network through a BLE-LoRa bridge—is a separate bearer design and is out of scope here. See BLE As A Local Bearer for the design space. This chapter reserves identifier space for that future work (see UUID Allocation) but does not specify it.

ULCP is transport-agnostic: frames are carried opaquely and unchanged. This binding replaces only the framing layer. HDLC-Lite framing (flags, escaping, and the FCS) is not used over BLE; ATT already provides reliable, ordered, integrity-protected delivery, and frame boundaries are recovered by the segmentation scheme below.

GATT Frame Transport

This section defines a generic, service-agnostic pattern for carrying delimited frames over GATT. The ULCP GATT Service (see ULCP GATT Service) instantiates it; a future local-bearer service may instantiate it independently.

A service using this pattern exposes a pair of characteristics:

CharacteristicDirectionGATT Properties
Frame InClient→ServerWrite; Write Without Response (optional)
Frame OutServer→ClientNotify

Each characteristic carries a sequence of segments. A segment is one ATT value: a single write to Frame In, or a single notification from Frame Out. One or more consecutive segments reassemble into exactly one frame. Segments of different frames are never interleaved on the same characteristic.

Segment Format

Every segment begins with a single header octet, followed by zero or more octets of frame data:

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
|  SAR  |       RESERVED        |
+---+---+---+---+---+---+---+---+

Figure: Segment Header Format

SAR: Segmentation and Reassembly

The two most significant bits indicate the segment’s position within its frame:

ValueNameMeaning
0SAR_COMPLETEThe segment contains a complete frame
1SAR_FIRSTFirst segment of a segmented frame
2SAR_CONTContinuation segment of a segmented frame
3SAR_LASTLast segment of a segmented frame

RESERVED: Reserved

The six least significant bits MUST be transmitted as zero. A receiver encountering a nonzero value MUST discard the segment and reset reassembly on that characteristic (see Reassembly).

Segmentation

A sender MUST NOT produce a segment larger than the current usable ATT payload (ATT_MTU minus 3 octets for the ATT opcode and handle). When a frame plus its one-octet segment header fits in a single ATT value, the sender SHOULD emit it as one SAR_COMPLETE segment. Otherwise the frame is split, in order, into one SAR_FIRST segment, zero or more SAR_CONT segments, and one SAR_LAST segment.

All segments of a frame MUST be sent before any segment of the next frame on the same characteristic.

Reassembly

The receiver maintains one reassembly buffer per characteristic:

  • SAR_COMPLETE: any partially reassembled frame is discarded; the segment payload is delivered as a complete frame.
  • SAR_FIRST: any partially reassembled frame is discarded; the segment payload starts a new reassembly.
  • SAR_CONT, SAR_LAST: the segment payload is appended to the reassembly in progress. If no reassembly is in progress, the segment MUST be discarded. On SAR_LAST, the reassembled octets are delivered as one complete frame.

The service instantiating this pattern defines the maximum reassembled frame size. If a reassembly exceeds it, the receiver MUST discard the partial frame and ignore subsequent SAR_CONT/SAR_LAST segments until the next SAR_COMPLETE or SAR_FIRST segment.

An ATT value of zero length contains no segment header and is not a valid segment; the receiver MUST discard it and reset reassembly on that characteristic.

Reassembly state is reset whenever the connection drops or the link detaches (see Attach Semantics).

Discarded segments and frames are transport-level events; they do not generate protocol-level error responses.

Flow Control

Client-to-server flow control uses the ATT write mechanism: a client using Write (with response) SHOULD NOT issue the next write until the previous response arrives. The server’s write response is its assertion that it has accepted the segment. A client MAY use Write Without Response where supported, in which case it relies on link-layer backpressure; servers MUST process such segments in order but MAY stall the bearer while doing so.

Server-to-client flow control is provided by the notification mechanism: the server’s stack paces notifications to the connection, and the server MUST NOT drop segments of a frame it has begun to send.

MTU Considerations

The scheme is correct at any ATT_MTU, including the 23-octet minimum. Clients SHOULD negotiate the largest ATT_MTU they support before attaching; servers SHOULD support an ATT_MTU of at least 247. Larger MTUs only reduce segment count—they never change frame semantics.

ULCP GATT Service

The ULCP GATT Service carries ULCP frames using the GATT frame transport defined above. One reassembled frame is exactly one ULCP frame as defined in Frame Format; the transport never inspects or modifies frame contents.

The maximum reassembled frame size for this service is 512 octets.

UUID Allocation

UMSH GATT identifiers are allocated from the randomly generated UMSH base UUID 21EB6B15-XXXX-4CCF-92E4-A079171BEC97, where XXXX is the assignment slot.

SlotUUIDAssignment
0x000121EB6B15-0001-4CCF-92E4-A079171BEC97ULCP GATT Service
0x000221EB6B15-0002-4CCF-92E4-A079171BEC97Frame In characteristic
0x000321EB6B15-0003-4CCF-92E4-A079171BEC97Frame Out characteristic
0x0100+—Reserved: BLE local bearer

Slots 0x0100 and above are reserved for the future BLE local-bearer service family and MUST NOT be used for tethered ULCP purposes.

Attach Semantics

A host is attached once it has enabled notifications on Frame Out (by writing the Client Characteristic Configuration Descriptor) over a connection meeting the security requirements in Security. Connection alone does not attach.

On attach, the device MUST silently reset its protocol session state—transaction correlation, reassembly, and session-scoped properties—and MUST NOT modify any other state: device and host provisioning, the RF configuration, and the PHY enable state are unaffected, and the radio keeps operating through the attach (see Attach, Detach, and Synchronization). No unsolicited notification is emitted on attach; the host learns the device’s current state by fetching it. The device MUST NOT emit any frame before attach.

A host detaches by disabling notifications or by disconnecting. Partially reassembled frames are discarded on detach.

The device supports one attached host at a time, across all transports it exposes. If a new host attaches—over BLE or over another transport such as USB—the device MUST detach any previously attached host and reset the session for the new one. A device MAY instead reject new connections while a host is attached.

Connection Parameters

The transport is latency-tolerant; any standard connection parameters work. Devices SHOULD accept connection intervals in the 15–50 ms range so that transmit confirmations and received-frame delivery do not dominate MAC-layer timing budgets.

Advertising and Discovery

Advertising MUST NOT begin until persisted configuration has been restored or startup has resolved the applicable defaults. The first advertising payload and BLE enablement decision MUST use that resolved configuration.

While BLE is enabled and transport arbitration permits it, a disconnected device SHOULD advertise as connectable and undirected. Advertising SHOULD suspend during a BLE connection or an attached wired session, then resume when permitted. Hosts discover devices by the ULCP service UUID; bonded hosts reconnect using their retained peer identity.

Outside pairing mode, advertising data MUST contain only generic BLE flags and the ULCP GATT Service UUID, with no local name, service data, manufacturer data, appearance or per-device identifier. Scan-response data MUST be empty. The flags omit general and limited discoverability; the device remains connectable so bonded hosts can reconnect. Previously installed scan-response data MUST be explicitly cleared when returning to this mode. The advertising payload MUST also be replaced to remove the model identifier and any name advertised during pairing.

During pairing mode, the device declares general discoverability, MUST include the ULCP GATT Service UUID in its advertising data, and SHOULD advertise its model identifier and the current PROP_DEV_NAME, shortening the name without splitting a grapheme cluster where needed. A rename during the window updates these payloads. Names, including existing hardware-derived default suffixes, are a deliberate disclosure during pairing. Ordinary renaming does not disconnect a connected host.

Devices SHOULD use resolvable private addresses (RPAs) with a persisted local Identity Resolving Key (IRK). Devices using RPAs MUST NOT fall back to advertising a permanent address when privacy setup fails. Entering or leaving a named pairing window SHOULD refresh the advertising address, deferring the refresh until disconnection if a connection is active. Address refresh MUST NOT extend pairing deadlines or reset authentication failures or lockout.

Model Identifier

A host platform can hand an application the advertising data alone, without the scan response, when the application is choosing a device to pair. The advertising data therefore identifies the hardware model, in a Manufacturer Specific Data element, and the name SHOULD travel in the scan-response data, where the ULCP GATT Service UUID does not compete with it for room.

OctetsFieldValue
0–1Company Identifier0xFFFF, least significant octet first
2–3Model IdentifierBig-endian
Model IdentifierMeaning
0x0000Unspecified
0x0001–0xFDFFAssigned, as model_id in the board list below
0xFE00–0xFEFFAssigned, as model_id in the platform list below
0xFF00–0xFFFFPrivate use: hardware with no assignment
{
  "boards": [
    { "id": "techo", "model_id": 1, "name": "T-Echo", "vendor": "LilyGO", "photo": "board-techo" },
    { "id": "t1000e", "model_id": 2, "name": "SenseCAP T1000-E", "vendor": "Seeed Studio", "photo": "board-t1000e" },
    { "id": "sensecap-solar", "model_id": 3, "name": "SenseCAP Solar Node P1", "vendor": "Seeed Studio", "photo": "board-sensecap-solar" },
    { "id": "wio-tracker-l1", "model_id": 4, "name": "Wio Tracker L1", "vendor": "Seeed Studio", "photo": "board-wio-tracker-l1" },
    { "id": "xiao-nrf52", "model_id": 5, "name": "XIAO nRF52840 + Wio-SX1262 Kit", "vendor": "Seeed Studio", "photo": "board-xiao-nrf52" },
    { "id": "heltec-v2", "model_id": 6, "name": "WiFi LoRa 32 V2", "vendor": "Heltec", "photo": "board-heltec-v2" },
    { "id": "heltec-v3", "model_id": 7, "name": "WiFi LoRa 32 V3", "vendor": "Heltec", "photo": "board-heltec-v3" },
    { "id": "tbeam-supreme", "model_id": 8, "name": "T-Beam Supreme", "vendor": "LilyGO", "photo": "board-tbeam-supreme" },
    { "id": "tlora-pager", "model_id": 9, "name": "T-LoRa Pager", "vendor": "LilyGO", "photo": "board-tlora-pager" },
    { "id": "xiao-esp32s3", "model_id": 10, "name": "XIAO ESP32S3 + Wio-SX1262 Kit", "vendor": "Seeed Studio", "photo": "board-xiao-esp32s3" },
    { "id": "tdeck", "model_id": 11, "name": "T-Deck", "vendor": "LilyGO", "photo": "board-tdeck" }
  ]
}

A platform identifier names a host with no fixed radio hardware, such as a phone running a UMSH application. It appears where a node names its model, such as a Meet Hello, and never in advertising data.

{
  "platforms": [
    { "id": "iphone", "model_id": 65024, "name": "iPhone", "symbol": "iphone" },
    { "id": "ipad", "model_id": 65025, "name": "iPad", "symbol": "ipad" },
    { "id": "android-phone", "model_id": 65026, "name": "Android phone", "symbol": "smartphone" },
    { "id": "android-tablet", "model_id": 65027, "name": "Android tablet", "symbol": "ipad.landscape" },
    { "id": "mac", "model_id": 65028, "name": "Mac", "symbol": "laptopcomputer" },
    { "id": "computer", "model_id": 65029, "name": "Computer", "symbol": "desktopcomputer" }
  ]
}

A model identifier names the same hardware as PROP_DEV_MODEL. A device with no fixed model sends 0. Octets after the model identifier are reserved: senders MUST NOT append any, and a host ignores any it finds. A host that does not recognize the model identifier, or finds no such element, presents the device without a model.

0xFFFF is not unique to UMSH. A host MUST interpret this element only in an advertisement that also carries the ULCP GATT Service UUID.

A host platform can withhold a device whose advertising data names a company identifier the host application has not declared. During pairing mode the advertising data MUST NOT carry any other Manufacturer Specific Data element.

Connected Device Names

The GAP Device Name characteristic exposes the configured name only while pairing, or on an encrypted connection backed by a durable, retained bond. Other name reads MUST receive an ATT security error. This applies to direct and offset reads, reads by type, multiple-value reads and name equality probes. Authenticated ULCP PROP_DEV_NAME behavior is unchanged.

The GAP value SHOULD reflect the current configured name. A name-value change is not a change to the service structure. Ordinary renaming MUST NOT require re-pairing.

Address rotation does not make a device anonymous to a host that holds its IRK. It reduces tracking by observers without that key. A deliberate pairing disclosure and continuous observation around a transition can still correlate activity; advertising frequency is not an anonymity guarantee.

Capabilities

CodeNameRequiresGrants
50CAP_BLE—A Bluetooth transport whose reachability the device can turn on and off, and which reports what is on it: PROP_BLE_ENABLED, PROP_BLE_LINK

A device implementing this binding MAY advertise CAP_BLE. Not advertising it means the transport is always reachable while the device is powered, which is what every device did before the capability existed; it does not mean the device has no BLE.

CAP_BLE is the only capability this binding defines, and everything else about a transport is discovered by asking for it. A device that manages its own bonds answers PROP_BLE_BOND_COUNT and PROP_BLE_PAIRING; one whose bonds are reachable only by a gesture at the device itself answers STATUS_PROP_NOT_FOUND to both.

This is deliberate. A capability is worth a code when a host would otherwise have to guess, and here it would not: the refusal is a complete answer, arrives in the same exchange the host was already making, and is a case the host must handle regardless—any property may be refused by firmware older than the host that asks. Splitting the transport into finer capabilities would buy a host nothing it cannot learn in the reply it is already waiting for, at the cost of a second claim that can disagree with the first.

Reachability

PROP 4871: PROP_BLE_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_BLE
  • Value Type: BOOL
  • Post-Reset Value: 1 (true), or restored from saved state

Whether the device is reachable over this transport. Cleared, the device MUST stop advertising and MUST drop any host attached over BLE; set again, it advertises as it did before. Bonds are untouched in both directions, so a bonded host reconnects without pairing again.

It says nothing about the radio itself. A device MAY power the controller down behind this and MAY leave the whole stack running; what the property promises is reachability, which is what an operator turning it off is asking about. Claiming the radio is off would be a claim most platforms cannot honor—a vendor stack that cannot be torn down at runtime is common—and a property that lies in the direction of “more private than it is” is the wrong one to guess at.

Asynchronous for the same reason PROP_GNSS_ENABLED is: a device MAY offer this as a control the operator can reach, and a switch someone can flip is a value that moves without the host asking. A device that flips it locally MUST publish the new value like any other transition the host did not command—which, when it is being cleared, is the last thing the attached host hears.

The post-reset value is true. A device unreachable by default is a device that cannot be configured by the host that would make it reachable again, and on most hardware the only other way in is the menu on the front of it.

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_BLE
  • Value Type: UINT8
  • Post-Reset Value: what the transport is doing
ValueNameMeaning
0BLE_LINK_NONENothing is connected over Bluetooth
1BLE_LINK_CONNECTEDA central holds a connection but has not attached
2BLE_LINK_ATTACHEDA host is attached and running ULCP over Bluetooth

How far the transport has got with whoever is on the other end of it. A device MUST publish the new value when it changes: a host arriving or walking away is a transition nobody asked for, and one that a watching administrator would otherwise have to poll for.

Connected and attached are separate values because they are separate facts. A central can hold the device’s connection without ever subscribing to the ULCP notification characteristic—a stalled pairing, an operating system reconnecting a bond in the background, or a host that simply occupies the slot—and a device with one peripheral connection is unreachable by anyone else while that lasts. Reporting that as “nobody is here” would describe a device that is in fact unavailable.

Read over BLE the value is always BLE_LINK_ATTACHED, because the session asking is the session it reports. The property earns its keep on the other bindings: over a serial transport and over Node Management it is the only way to ask whether someone is on the device’s Bluetooth right now.

Like the bond count, it is live transport state: NOT part of the saved snapshot, and CMD_RST MUST NOT change it. A host does not disconnect because a reset was performed on the device it is attached to.

It says what the transport is doing, never with whom. Identifying the connected host would leak the same association the bond count withholds.

Bond Management

PROP 4872: PROP_BLE_BOND_COUNT

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_BLE
  • Value Type: UINT8
  • Post-Reset Value: the number of bonds the device holds

How many hosts are currently bonded. A device MUST report the count its durable bond store holds, and MUST publish the new value when it changes—enrollment and eviction both happen without the host asking, so a host that was not told would have to poll.

A device that does not manage its own bonds answers STATUS_PROP_NOT_FOUND, which is how a host learns that neither half of bond management is available to it.

The count is live transport state, not configuration: it is NOT part of the saved snapshot, and CMD_RST MUST NOT change it. A protocol reset returns protocol state to its post-reset values, and a bond is neither protocol state nor something a reset deletes.

It says how many hosts are enrolled, never which. A device that named its bonded hosts to whoever asked would leak the association the pairing ceremony exists to protect, and the count is what an operator deciding whether to clear bonds actually needs.

Writing Zero: Forgetting Every Host

Writing 0 atomically persists an empty bond store, a cleared pairing PIN, and a fresh, nonzero local IRK different from the previous IRK. A failed commit leaves the previous security state intact. After success, it clears the pairing failure lockout and opens a pairing window. It does not enable BLE if BLE was disabled, or change non-BLE configuration or device identity.

Zero is the only value a host may write, and any other MUST be answered STATUS_INVALID_ARGUMENT. Bonds are enrolled one pairing ceremony at a time and evicted by the device, so no other count names a state a device could be put into: a host asking for three bonds is not describing anything the device could do.

The device MUST NOT answer before the deletion is durable, and MUST drop the deleted bonds from any live in-memory bond table as well as from durable storage—a bond forgotten on flash but still held in RAM would keep working until the next boot. It MUST then enter pairing mode: a device that has forgotten every host it trusts and is not accepting new ones is reachable by nothing.

The write is answered like any other, with the property’s value: a CMD_PROP_IS carrying 0 once the deletion is durable. Sent over BLE, that answer is the last thing the sender hears, because the bond that carried it is among the bonds deleted; the reply MUST be emitted before the connection is dropped. Queue insertion alone MUST NOT be treated as completed transmission. Failure to deliver the reply MUST NOT undo durable revocation. Further GATT access MUST be denied after commit, and the new IRK MUST be installed before further advertising or pairing.

Clearing the PIN alongside the bonds is deliberate. A PIN outliving the hosts it was set for would leave a device that has forgotten everyone still demanding a secret the operator may no longer have, recoverable only by a local wipe.

PROP_BLE_ENABLED does not gate this write, which is where it parts company with PROP_BLE_PAIRING. Bonds are durable state rather than reachability: a device with the transport turned off still holds them and still counts them, and deleting them is exactly as meaningful there as it is with a host connected. The pairing mode the deletion leaves behind is then a window onto a transport that is down, which PROP_BLE_PAIRING reports as closed like any other.

Forgetting every host is a write rather than a command, and rather than a reset-class one, because the count is already the state it changes. A command would have had to be answered by a status saying what the property could say by quoting itself, and a host reading the count back is asking the same question the answer already contains.

PROP 4874: PROP_BLE_PAIRING

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: Yes
  • Required: CAP_BLE
  • Value Type: BOOL
  • Post-Reset Value: whether the window is open

Whether pairing mode is active. Writing 1 opens a window, so an unbonded host may pair without a physical gesture at the device; writing 0 closes one. The window is a property rather than a command because it is a state with more ways out than in—the write, a timeout, a completed bond—and only a property can be closed again, read back, and reported moving on its own.

The device MUST publish the new value on any transition the writer did not just command: expiry and a new bond both close the window with no host asking, and a physical gesture at the device opens one.

A write of 1 answers STATUS_INVALID_STATE when no window can open: the device is locked out after repeated pairing failures, or Bluetooth is off (PROP_BLE_ENABLED is 0)—nothing can pair through a transport that is down, and a device MUST NOT report a window nothing can walk through. A full bond store is NOT a refusal: enrollment at capacity evicts rather than refuses (see Bond Management), so a device with a full store opens the window like any other. A write of 0 MUST succeed: there is no state in which a window refuses to shut.

A device that does not manage its own bonds answers STATUS_PROP_NOT_FOUND, like the bond count.

Like the bond count and the link, the window is live transport state: NOT part of the saved snapshot, and CMD_RST MUST NOT touch it.

Security

ULCP is a privileged interface: an attached host commands transmission with arbitrary content, timing, and power under the operator’s regulatory responsibility, observes all traffic metadata the radio receives, and can deny service to the legitimate host. On serial transports this interface is implicitly protected by physical possession of the device. The BLE binding MUST provide at least an equivalent barrier, and its cryptographic strength MUST NOT fall below that of UMSH’s identity layer (approximately 128-bit, bounded by Curve25519; see Security & Cryptography).

Pairing Requirements

  • Pairing MUST use LE Secure Connections (LESC). Legacy pairing MUST be rejected; Devices SHOULD operate in Secure Connections Only mode.
  • Bonding is REQUIRED. The device MUST NOT attach a host over an unbonded link.
  • The Frame In and Frame Out characteristics, including the Frame Out CCCD, MUST be readable and writable only over an encrypted link keyed by a stored LESC bond. Access over any other link MUST be refused with the appropriate ATT security error.

LESC pairing (P-256 ECDH) meets the 128-bit strength requirement. The remaining risk is man-in-the-middle interception during the pairing ceremony itself, which the following requirements bound.

Pairing Mode

Except as provided for OOB pairing below, the device accepts pairing requests from unbonded devices only while in pairing mode; at all other times such requests MUST be rejected. A configured pairing PIN selects the association model but does not bypass the pairing-mode requirement.

Entering pairing mode:

  • While the device holds no bonds, it SHOULD enter pairing mode automatically at power-on.
  • Once the device holds one or more bonds, it MUST NOT enter pairing mode automatically. Entering pairing mode then requires either a deliberate physical gesture distinct from normal power-on—for example, holding the user button through power-on until the device signals that pairing mode is active—or a write of 1 to PROP_BLE_PAIRING from an authorized session.

A timeout of five minutes with no bonds, or two minutes with one or more bonds, is RECOMMENDED, based on the bond count when the window opens.

Pairing mode MUST end when any of the following occurs:

  • a new bond completes;
  • an already-bonded host establishes an encrypted connection;
  • an implementation-defined timeout expires.

The device SHOULD give a perceptible indication (LED pattern, tone, or display) while pairing mode is active.

A physical-presence-gated ceremony reduces the pairing trust decision to possession of the device—the same property that protects the serial transports. A command from an already-authorized session is the same decision made by someone who has already passed that ceremony: an attached host holds a bond, and a mesh administrator is listed in PROP_DEV_ADMINS, which is itself set through an attached session. What the gesture proves about a person standing at the device, authorization proves about a party that was admitted earlier; a party that can already administer the device gains nothing by opening a window it could open by walking over.

Association Models

  • Devices with a display and a confirmation input SHOULD use an authenticated association model (numeric comparison, or passkey display) for new bonds.
  • Devices without a display, and with no pairing PIN configured, use Just Works, accepted only in pairing mode. This model is unauthenticated; the pairing-mode gesture is the entire trust decision.
  • Devices with a pairing PIN configured (see Pairing PIN Configuration) use LESC Passkey Entry with the configured PIN as a static passkey. New bonds still MUST be accepted only while in pairing mode. The device MUST count consecutive passkey authentication failures—pairing attempts that fail the LESC confirm-value or DHKey check—since power-on; the counter resets on a successful pairing or a power cycle. Rejections that never reach passkey authentication (legacy-pairing attempts, pairing refused outside pairing mode, malformed pairing requests) MUST NOT increment the counter, so they cannot be used to lock out pairing remotely. After a small limit (MUST NOT exceed 5; 3 RECOMMENDED), the device MUST reject all further pairing attempts until it is power cycled.
  • Devices MAY additionally support LESC Out-of-Band pairing (for example, OOB data conveyed by a QR code affixed to or displayed by the device). OOB pairing is authenticated and MAY be accepted at any time. The conveyance and provisioning of OOB data is out of scope for this document.

The failed-attempt lockout is load-bearing, not defensive polish: LESC Passkey Entry discloses the passkey one bit per protocol round, so an active attacker learns roughly one PIN bit per failed pairing attempt, and a passive eavesdropper on one successful pairing learns the entire PIN. A static passkey therefore provides bounded, not absolute, authentication: the lockout caps active extraction at a few bits per power cycle performed by the operator, and operators SHOULD change the PIN if a pairing exchange may have been observed.

Pairing PIN Configuration

PROP 4864: PROP_BLE_PAIRING_PIN

  • Type: Single-Value, Write-Only
  • Asynchronous Updates: No
  • Required: OPTIONAL (meaningful only on devices exposing this transport)
  • Value Type: UINT32_LE, or empty
  • Units: LESC passkey, decimal 0–999999
  • Post-Reset Value: Persisted

Sets the static passkey used by the configured-PIN association model above. Writing an empty value clears the PIN, returning the device to the Just Works model. Values outside 0–999999 fail with STATUS_INVALID_ARGUMENT.

As an exception to the usual CMD_PROP_SET behavior, a successful set of this property is acknowledged with CMD_PROP_IS for PROP_LAST_STATUS carrying STATUS_OK and the command’s TID; the device MUST NOT emit CMD_PROP_IS for this property itself. Success MUST NOT be reported before the new value is in effect for subsequent pairing attempts and, where the device supports persistence, durably stored; a value that cannot be applied or stored is reported with an appropriate error status and leaves the previous PIN state unchanged.

The PIN persists across resets and power cycles. It is write-only: CMD_PROP_GET for this property MUST fail with STATUS_UNIMPLEMENTED and MUST NOT disclose the value or whether a PIN is configured.

Because this property is only reachable through an attached session, it is always protected by the transport that carried it: physical possession on serial transports, or an existing bonded LESC link on BLE.

Bond Management

  • Devices MUST provide a local mechanism to delete stored bonds. The mechanism is implementation-specific. Deletion MUST NOT be invocable over an unauthenticated path; a device that manages its own bonds additionally accepts a write of zero to PROP_BLE_BOND_COUNT, whose authorization is the session’s own (see Administrative Authorization).
  • Devices MAY limit the number of stored bonds. A full bond store MUST NOT cause pairing to be refused: when a new bond is enrolled while the store is full, the device MUST evict the least-recently-used bond to make room. Bond-store capacity MUST NOT appear as a term in the pairing admission decision.
  • A device that evicts by recency MUST record use: the recency order MUST be updated when a bonded host establishes an encrypted connection, not only when a bond is created or refreshed. A store ordered only by enrollment evicts the wrong bond.
  • A device that evicts a bond from durable storage MUST also drop it from any live in-memory bond table, so the evicted peer cannot continue to reconnect as bonded for the remainder of the power cycle.

Refusing enrollment at capacity would be the more restrictive-looking choice and is the wrong one: it turns a full store into a state from which the device can only be recovered by a local wipe, while enrollment already requires physical presence and the eviction victim is by construction the bond that has gone longest without connecting.

Removing one specific bond while retaining the others is not expressible through this protocol, which is also why zero is the only count a host may write. The count written to zero forgets every host at once, and that is the operation an operator reaching for it wants: the case that motivates it is a device whose paired hosts are no longer trusted or no longer known, and enumerating bonds so one could be named would mean reporting which hosts a device has met.

Administrative Authorization

Any retained secure bond may administer the device. There is no per-host privilege distinction: a host that has completed the pairing ceremony and holds a stored bond has the same authority as any other, for as long as its bond is retained.

Equivalently, possession of a serial transport confers the same authority. The serial transports have no cryptographic admission step at all, so a host that can open the port is attached; this is the MUST in Pairing Requirements read the other way—BLE is required to reach the barrier that physical possession already provides, not to exceed it.

The consequences worth stating plainly:

  • Enrollment, not authorization, is the gate. Reaching the pairing ceremony requires pairing mode, and pairing mode requires physical presence (see Pairing Mode).
  • Administering a device does not make the administering host that device’s tethered host, and MUST NOT cause the device to adopt it as one. See Local Control Protocol for the distinction between administrative and tethered attach.
  • All transports implementing this protocol MUST apply the same rules. A device MUST NOT grant a capability over one transport that it withholds over another.

Layering Note

BLE link security protects the transport. It does not alter the UMSH security model: MAC-layer keys remain on the host, frames crossing this link remain UMSH ciphertext where UMSH encrypts them, and a compromised device still cannot impersonate host identities. Conversely, future ULCP extensions that provision keying material to the device (see Security Boundary) MUST NOT be carried over a link that does not meet the requirements of this section.

ULCP: Minimal Protocol

Note

This chapter has been superseded and is retained for reference. Its content now lives in Framing and Common Semantics, Radio Control, and Frame Transport. What a device is required to implement—the question this chapter’s minimal/full split used to answer—is stated in Minimum Requirements, and every numeric identifier is listed in the Command and Property Index.

The UMSH Local Control Protocol (ULCP) is inspired by the Spinel protocol from OpenThread, but it is not Spinel and does not aim for wire compatibility with it. The protocol assumes reliable, in-order delivery of frames, as well as a way to assert flow control. The framing mechanism depends on the underlying transport:

  • Asynchronous serial links (UART, USB-CDC) use HDLC-Lite, exactly as used by Spinel.
  • BLE uses the GATT frame transport defined in ULCP over BLE.

The specific subset of the protocol documented here is the minimal set needed to configure and use a LoRa radio. All other concerns are left out to make it easier to implement this initial step. The full protocol is a strict superset of this document, adding receive filtering, inbound queueing, key provisioning, and acknowledgement delegation.

In this document, the device is the side that owns the transceiver and the host is the side that attaches to it over the local link (see Local Control Protocol).

Data Representation

Spinel, being a low-level protocol between two devices which are likely to have a little-endian architecture, uses little-endian representations exclusively for all integers smaller than four bytes. For implementation convenience, values larger than four bytes (EUI64, IPv6 addresses, etc.) are stored as they are traditionally represented (typically, but not always, big-endian).

Packed Unsigned Integers

Certain types of integers, such as command or property identifiers, usually have a value on the wire that is less than 127. However, in order to not preclude the use of values larger than 255, we would need to add an extra byte. Doing this would add an extra byte to all packets, which can add up in terms of bandwidth. To address this, Spinel uses Packed Unsigned Integers, or PUIs.

The PUI format used in Spinel is based on the unsigned integer format in EXI, except that we limit the maximum value to the largest value that can be encoded in three bytes. The maximum value that can be encoded is 2,097,151.

For all values less than 127, the packed form of the number is simply a single byte which directly represents the number. For values larger than 127, the following process is used to encode the value:

  1. The unsigned integer is broken up into n 7-bit chunks and placed into n bytes, leaving the most significant bit of each byte unused.
  2. Order the bytes from least-significant to most-significant. (Little-endian)
  3. Clear the most significant bit of the most significant byte. Set the most significant bit on all other bytes.

Where n is the smallest number of 7-bit chunks you can use to represent the given value.

Take the value 1337, for example:

1337 => 0x0539
     => [39 0A]
     => [B9 0A]

To decode the value, you collect the 7-bit chunks until you find a byte with the most significant bit clear.

Frame Format

A ULCP frame is the concatenation of the following elements:

  • A header comprising a single byte.
  • A command identifier.
  • A command-defined payload, which may be empty.
  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     HEADER    |  COMMAND ID   | PAYLOAD ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of a typical ULCP frame

Since the size of the frame is part of the framing mechanism, it is omitted from the frame.

Frame Header

Each frame has the following format:

  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
|  FLG  | RESERVED  |    TID    |
+---+---+---+---+---+---+---+---+

Figure: Header Format

FLG: Flag

The Flag (FLG) field in the two most significant bits of the header byte is always set to the value two (or 10 in binary). Any frame received with these bits set to any other value SHALL NOT be considered a ULCP frame.

RESERVED: Reserved

These three bits must always be set to zero and the entire frame ignored if set to any other value. They may be assigned a meaning (such as an interface identifier) in a future version of this protocol.

TID: Transaction Identifier

The Transaction Identifier (TID) field in the three least significant bits of the header is used for correlating responses to the commands which generated them. This allows for up to seven host-issued commands to be in flight at once.

When a command is sent from the host, any reply to that command sent by the device will use the same value for the TID. When the host receives a frame that matches the TID of the command it sent, it can easily recognize that frame as the actual response to that command.

The zero value of TID is used for commands to which a correlated response is not expected or needed, such as for unsolicited update commands sent to the host from the device.

Note that while the frame format is symmetric between the frames being sent to the device versus frames being sent from the device, the behaviors are not. The device MUST NOT send a frame with a non-zero TID that is not a response to a frame it had recently received with that same TID. All unsolicited or asynchronous commands originating from the device MUST use TID zero (0).

Command ID

The command identifier is a 7-bit unsigned integer encoded from 0 to 127. The most significant bit is not set and the frame must be ignored if it is set.

Payload

The command payload follows the command identifier in a ULCP frame, containing the serialization of any arguments that the indicated command may require. The exact composition of a command payload is determined by the specific command identifier being used and MUST be empty if the command has no arguments.

Commands

The following commands are initially supported:

IdMnemonicDirDescription
0CMD_NOPHost->DeviceNo-Operation
1CMD_RSTHost->DeviceReset the device
2CMD_PROP_GETHost->DeviceGet property value
3CMD_PROP_SETHost->DeviceSet property value
6CMD_PROP_ISDevice->HostProperty value notification
9CMD_STR_SENDHost->DeviceSend data to a stream
10CMD_STR_RECVDevice->HostReceive data from a stream

Command identifiers 4, 5, 7, and 8 are assigned to property insert/remove operations and their corresponding notifications, defined in the full protocol. They are listed here as reserved because no property defined in this document uses them; a minimal-only device simply responds to them with STATUS_INVALID_COMMAND.

CMD 0: (Host -> Device) CMD_NOP

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_NOP    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
^     HEADER    ^    COMMAND    ^

Figure: Structure of CMD_NOP

No-Operation. Commands the device to reply with a STATUS_OK code. This is primarily used for liveness checks.

The command payload for this command SHOULD be empty. The receiver MUST ignore any non-empty command payload.

There is no error condition for this command.

CMD 1: (Host -> Device) CMD_RST

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_RST    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_RST

Reset device. Commands the device to perform a software reset. Due to the nature of this command, the TID is ignored. The host should instead wait for a CMD_PROP_IS command from the device indicating PROP_LAST_STATUS has been set to STATUS_RESET_SOFTWARE (see (#status-codes)).

The command payload SHOULD be empty, and it SHOULD NOT be processed.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 2: (Host -> Device) CMD_PROP_GET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_GET

Get property value. Commands the device to emit a CMD_PROP_IS command for the given property identifier.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in (#packed-unsigned-integer).

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 3: (Host -> Device) CMD_PROP_SET

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  NEW PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_SET

Set property value. Commands the device to set the given property to the specific given value, replacing any previous value, and to emit a CMD_PROP_IS command for that property indicating the new authoritative value if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in (#packed-unsigned-integer), followed by the property value. The exact format of the property value is defined by the property.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 6: (Device -> Host) CMD_PROP_IS

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  CURRENT PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_IS

Property value notification. This command can be sent by the device in response to a previous command from the host, or it can be sent by the device in an unsolicited fashion to notify the host of various state changes asynchronously.

The payload for this command is the property identifier encoded in the packed unsigned integer format described in (#packed-unsigned-integer), followed by the current value of the given property.

CMD 9: (Host -> Device) CMD_STR_SEND

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | STREAM_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   DATA_LEN (Little endian)    | DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_STR_SEND

Command for sending data (such as a packet) to a stream.

The format of the metadata is defined by the stream, and may be absent. Since the framing layer provides the total frame length, DATA_LEN is sufficient to determine the length of both the data and any trailing metadata.

If a non-zero TID is used, the command completes only once the frame has either been transmitted on air or definitively failed. Success is reported by emitting CMD_PROP_IS for PROP_LAST_STATUS with STATUS_OK and a matching TID.

The device only attempts one confirmed transmit at a time. If a CMD_STR_SEND with a non-zero TID arrives while another confirmed transmit is in progress, the new command fails with STATUS_BUSY.

The device will never wait for duty-cycle allowance. If transmission would exceed the currently configured duty-cycle limit and the NODUTY flag is not set, the command fails immediately with STATUS_DUTY_LIMIT.

Commands sent with TID zero are fire-and-forget and do not receive a correlated completion response.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 10: (Device -> Host) CMD_STR_RECV

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES |0 0 0|      CMD      | STREAM_KEY (PUI, 1-3 bytes)...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   DATA_LEN (Little endian)    | DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_STR_RECV

Notification of incoming data received on the given stream. Because this command is only ever sent asynchronously, the TID is always zero.

The format of the metadata is defined by the stream, and may be absent. Since the framing layer provides the total frame length, DATA_LEN is sufficient to determine the length of both the data and any trailing metadata.

Properties and Streams

Note

The properties marked as supporting Is means that the property may be emitted asynchronously. All properties that support Get or Set will emit an Is to respond with the current/new value of that property.

IdMnemonicCommandsDescription
0PROP_LAST_STATUSGet, IsLast status
1PROP_PROTOCOL_VERSIONGetProtocol version
2PROP_DEV_VERSIONGetDevice version string
3PROP_IFACE_TYPEGetInterface type
5PROP_CAPSGetCapabilities
32PROP_PHY_ENABLEDGet, SetPHY enabled
35PROP_PHY_FREQGet, SetFrequency in kHz
37PROP_PHY_TX_POWERGet, SetTX power in dBm
38PROP_PHY_RSSIGetCurrent RSSI
39PROP_PHY_LORA_BWGet, SetLoRa bandwidth
40PROP_PHY_LORA_SFGet, SetLoRa spreading factor
41PROP_PHY_LORA_CRGet, SetLoRa coding rate
42PROP_PHY_MTUGetMax size of a frame
43PROP_PHY_LORA_SWGet, SetLoRa sync word (16-bit style)
113STR_PHY_RAWSend, RecvRaw radio frame stream
4820PROP_PHY_DUTY_NOWGetCurrent duty usage
4822PROP_PHY_DUTY_LIMITGet, SetDuty-cycle limit

PROP 0: PROP_LAST_STATUS

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: REQUIRED
  • Value Type: PUI + STRING(opt.)
  • Units: Enumeration
  • Post-Reset Value: Reset Reason Code

This property describes the status code of the last device operation. For many commands, failure is indicated by emitting CMD_PROP_IS for this property with a TID matching the failing command. It is generally not necessary to ever fetch the value of this property explicitly, as it is often emitted directly as an error response. It is also occasionally emitted as a success response with a value of STATUS_OK.

Upon device reset, this property MUST be emitted with a status code indicating the reset reason.

Upon receiving an asynchronous update to PROP_LAST_STATUS with a status code that indicates a reset, the host SHALL assume that the device has been reset and that all properties have reverted to their defined after-reset values.

See (#status-codes) for the complete list of status codes.

PROP 1: PROP_PROTOCOL_VERSION

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: Device
  • Value Type: UINT8, UINT8
  • Post-Reset Value: 6, 0

Describes the ULCP version information. This property contains two fields:

  0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MAJOR_VERSION | MINOR_VERSION |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: PROP_PROTOCOL_VERSION Value Format

MAJOR_VERSION
The major version number is used to identify backward incompatible differences between protocol versions.
MINOR_VERSION
The minor version number is used to identify backward-compatible differences between protocol versions. A mismatch between the advertised minor version number and the minor version that is supported by the host SHOULD NOT be fatal to the operation of the host.

This document describes major version 6, minor version 0 of this protocol.

PROP 2: PROP_DEV_VERSION

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: Device
  • Value Type: STRING
  • Post-Reset Value: Implementation-Specific

Contains a zero-terminated ASCII string which describes the firmware currently running on the device.

The value of this string MUST be different for every firmware release.

The format of the string is not strictly defined, but it is intended to present similarly to the “User-Agent” string from HTTP. The following format is RECOMMENDED:

STACK-NAME/STACK-VERSION[BUILD-INFO][; OTHER-INFO][; BUILD-DATE]

PROP 3: PROP_INTERFACE_TYPE

  • Type: Single-Value, Constant
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: PUI
  • Units: Enumeration
  • Post-Reset Value: Implementation-Specific

This unsigned packed integer identifies the network protocol implemented by this device. It must return the value 8.

PROP 5: PROP_CAPS

  • Type: Multiple-Value, Constant
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Item Type: PUI
  • Units: Enumeration
  • Post-Reset Value: Implementation-Specific

Describes the supported capabilities of this device. Encoded as a list of packed unsigned integers. See (#capabilities) for a list of values.

PROP 32: PROP_PHY_ENABLED

  • Type: Single-Value, Read/Write
  • Asynchronous Updates: No
  • Required:
    • CMD_PROP_GET: REQUIRED
    • CMD_PROP_SET: REQUIRED
  • Scope: NLI
  • Value Type: BOOL
  • Post-Reset Value: 0 (false)

Set to 1 if the PHY is enabled, set to 0 otherwise. May be directly enabled to bypass higher-level packet processing in order to implement things like packet sniffers.

PROP 35: PROP_PHY_FREQ

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: UINT32_LE
  • Units: kHz
  • Post-Reset Value: Unspecified

Value is the radio frequency (in kilohertz) of the current channel.

PROP 37: PROP_PHY_TX_POWER

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: INT8
  • Units: dBm
  • Post-Reset Value: Implementation-Specific

Value is the radio transmit power in dBm.

PROP 38: PROP_PHY_RSSI

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Value Type: INT8
  • Unit: dBm (RF Power)
  • Post-Reset Value: Unspecified

Value is the current RSSI (Received Signal Strength Indication) from the radio. This value can be used in energy scans and for determining the ambient noise floor for the operating environment.

Zero dBm represents one milliwatt of power.

Sampling ambient RSSI requires the radio to be actively receiving. If PROP_PHY_ENABLED is false, getting this property fails with STATUS_INVALID_STATE. A get may also fail with STATUS_FAILURE if the radio cannot service the read (for example, mid-reconfiguration).

PROP 39: PROP_PHY_LORA_BW

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT32_LE
  • Units: Hz
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa bandwidth.

PROP 40: PROP_PHY_LORA_SF

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT8
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa spreading factor.

PROP 41: PROP_PHY_LORA_CR

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT8
  • Post-Reset Value: Implementation-Specific

Value is the configured LoRa coding rate.

PROP 42: PROP_PHY_MTU

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: REQUIRED
  • Scope: NLI
  • Value Type: UINT16_LE
  • Units: octets
  • Post-Reset Value: Implementation-Specific

Maximum size of the DATA field that may be supplied to STR_PHY_RAW.

PROP 43: PROP_PHY_LORA_SW

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_PHY_LORA
  • Scope: NLI
  • Value Type: UINT16
  • Post-Reset Value: Implementation-Specific, but 0x1424 is a good suggestion.

Value is the 16-bit (SX126x-style) LoRa sync-word.

STREAM 113: STR_PHY_RAW

  • Type: Packet-Stream, Input/Output
  • Required: REQUIRED
  • Supported Commands: Send, Recv
  • Scope: NLI
  • Value Type: Structure
  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   PACKET_LEN (Little endian)  | PACKET_DATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   PACKET_METADATA ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

This stream provides the capability of sending and receiving raw packets to and from the radio.

The packet metadata is optional, but if present will be after the packet data and will have the following format:

Metadata for Send

The Send metadata is the following fields in order:

  • TX_POWER (i8): Transmit power override (0x7F indicates to use the radio default, 0x7E indicates to transmit at maximum power)
  • TX_FLAGS (u8): Transmit flags
    • TX_FLAG_NOCCA Bit 0: If set, do not use CCA (or the equivalent LoRa mechanism)
    • TX_FLAG_NODUTY Bit 1: If set, send the packet even if it would push us over the duty-cycle limit
    • All other bits: RESERVED

Metadata for Recv

The Recv metadata is the following fields in order:

  • RX_RSSI (u8): This is the negative RSSI that this packet was received with. So if the RSSI was -91, the value of this field would be 91.
    • If 0xFF, no RSSI is supported.
  • RX_LQI (u8): This is the link-quality indicator, which is a metric of link quality between 1 and 255 with 1 being the worst possible quality that still decodes and 255 is perfect reception.
    • If 0x00, LQI is not supported.
  • RX_SNR (i16): Signal-to-noise ratio in centibels, or 1/10 of a decibel.
    • If 0x8000 (i16::MIN), SNR is not supported. This sentinel is chosen because it is -3276.8 dB, a value no real link can report, so it never collides with a genuine measurement (unlike 0xFFFF, which is -0.1 dB).

PROP 4820: PROP_PHY_DUTY_NOW

  • Type: Single-Value, Read-Only
  • Value Type: u16
  • Units: Percent, 0-65535 -> 0-100%
  • Post-Reset Value: 0%
  • Required Capability: CAP_PHY_DUTY_LIMIT

The radio transmit duty cycle over the past hour, updated in 4-minute intervals.

Under the hood, this is represented as 15 16-bit bins, one for each 4-minute interval. An increment of 1 represents 5ms. For each 5ms of transmission time, the current bin is incremented by 1. So a 20ms transmission would increment the current bin by 4, but a 22ms transmission would increment the bin by 5. At the transition between intervals, the new current bin is reset to zero.

To calculate the current duty cycle, all of the bins are added together, multiplied by 65535, and then divided by 720000.

PROP 4822: PROP_PHY_DUTY_LIMIT

  • Type: Single-Value, Read-Write
  • Value Type: u16
  • Units: Percent, 0-65535 -> 0-100%
  • Post-Reset Value: Settings-dependent
  • Required Capability: CAP_PHY_DUTY_LIMIT

The value for PROP_PHY_DUTY_NOW above which sending additional packets will be prevented. Packets that are prevented from being sent will be dropped with STATUS_DUTY_LIMIT.

Set to 0xFFFF to disable duty-cycle limiting. Note that PROP_PHY_DUTY_NOW will continue to be updated even if duty-cycle limiting is disabled.

Values for common duty cycles:

ValuePercentage
1310720%
655310%
6551%
650.1%

Status Codes

Status codes are used for PROP_LAST_STATUS. When a command generates a status code, it is returned via a CMD_PROP_IS with a property of PROP_LAST_STATUS and the TID of command it is referring to.

IdName
0STATUS_OK
1STATUS_FAILURE
2STATUS_UNIMPLEMENTED
3STATUS_INVALID_ARGUMENT
4STATUS_INVALID_STATE
5STATUS_INVALID_COMMAND
7STATUS_INTERNAL_ERROR
9STATUS_PARSE_ERROR
10STATUS_IN_PROGRESS
11STATUS_NOMEM
12STATUS_BUSY
13STATUS_PROP_NOT_FOUND
18STATUS_CCA_FAILURE
32STATUS_DUTY_LIMIT
STATUS_OK
Indicates that the operation has completed successfully.
STATUS_FAILURE
Indicates that the operation has failed for an unspecified reason. The use of this status code SHOULD be avoided. If a more specific status code exists that better explains the failure, then that status code MUST be used instead.
STATUS_UNIMPLEMENTED
Indicates that the given operation has not been implemented.
STATUS_INVALID_ARGUMENT
Indicates that an argument to the given operation is invalid. The value may be out of range or improperly formatted. This status code is also returned when setting an invalid value to a property.
STATUS_INVALID_STATE
Indicates that the given operation is invalid for the current state of the device.
STATUS_INVALID_COMMAND
The given command id is not recognized.
STATUS_INTERNAL_ERROR
An internal runtime error has occurred.
STATUS_PARSE_ERROR
An error has occurred while parsing the command.
STATUS_NOMEM
The operation has been prevented due to memory pressure.
STATUS_BUSY
The device is currently performing a mutually exclusive operation. This status differs from STATUS_INVALID_STATE in that it will resolve spontaneously.
STATUS_PROP_NOT_FOUND
The given property key is not recognized.
STATUS_CCA_FAILURE
The packet was not sent due to a CCA failure. This status code is only emitted when sending data to a packet stream with a TID other than zero.
STATUS_DUTY_LIMIT
The packet cannot be sent because it would exceed the currently set duty-cycle limit.

Reset Codes

All status codes which fall into the inclusive range of 112-127 are considered reset codes. These codes are emitted asynchronously after a device reset and provide a way to differentiate different causes of resets. If the first command the host sends to the device after a reset is to fetch PROP_LAST_STATUS, then the reset code MUST be returned.

Note

On a device implementing the full protocol that holds a saved snapshot, the post-reset value of every saved property is its saved value rather than the documented default. A host MUST NOT assume that a reset implies documented factory defaults; it should fetch or explicitly set the properties it depends on. Without a snapshot—in particular on any minimal-only device—the documented post-reset values apply unconditionally.

IdName
112STATUS_RESET_POWER_ON
113STATUS_RESET_EXTERNAL
114STATUS_RESET_SOFTWARE
116STATUS_RESET_CRASH
117STATUS_RESET_ASSERT
118STATUS_RESET_OTHER
119STATUS_RESET_UNKNOWN
120STATUS_RESET_WATCHDOG

Of these defined reset codes, only STATUS_RESET_POWER_ON, STATUS_RESET_EXTERNAL, and STATUS_RESET_SOFTWARE are emitted during normal operation. All other reset codes generally indicate some sort of software bug or hardware failure.

Unexpected or unrequested resets are always an indication of a problem, no matter what the code value is.

STATUS_RESET_POWER_ON
Cold power-on start.
STATUS_RESET_EXTERNAL
External device reset. This is generally caused by RESET pin on the device being asserted.
STATUS_RESET_SOFTWARE
Software-requested orderly reset. This is generally caused by the host sending the device CMD_RST.
STATUS_RESET_CRASH
Unrecoverable software execution failure, like a segmentation fault or a stack overflow.
STATUS_RESET_ASSERT
Software invariant property not respected.
STATUS_RESET_OTHER
Unspecified cause.
STATUS_RESET_UNKNOWN
Failure while recovering cause of reset.
STATUS_RESET_WATCHDOG
Watchdog timer expired, forcing a reset.

Capabilities

Capabilities are how a device can advertise support for specific behaviors and functionalities. They can be fetched via the PROP_CAPS property.

See (#prop-caps) for more information on PROP_CAPS.

CodeName
8CAP_WRITABLE_RAW_STREAM
16CAP_PHY_DUTY_LIMIT
515CAP_PHY_LORA

ULCP: Full Protocol

Note

This chapter has been superseded and is retained for reference. Its content now lives in Framing and Common Semantics, Device Domain, Saved State, and Tethered Host Services. What a device is required to implement—the question this chapter’s minimal/full split used to answer—is stated in Minimum Requirements, and every numeric identifier is listed in the Command and Property Index.

This chapter defines the full ULCP protocol: a strict superset of the minimal protocol. A device implementing this chapter implements everything in the minimal protocol— the frame format, packed unsigned integers, commands, properties, status codes, reset codes, and capabilities defined there apply here unchanged and are not repeated. The protocol version remains 6.0; a host discovers which full-protocol features a device implements through PROP_CAPS (see (#full-capabilities)), not through the version number.

The minimal protocol treats the device as a raw radio pipe: the host runs the entire UMSH MAC and the device moves frames. The full protocol keeps that division of labor—the host still owns the MAC and its own private keys— and adds narrowly scoped assistance so the device can be useful while the host is asleep or disconnected:

  • Receive filtering—the device learns which frames are relevant so it does not deliver (or wake the host for) unrelated traffic.
  • Inbound queueing—frames received while no host is attached are retained and delivered when the host asks for them.
  • Key provisioning—the host installs channel keys and pairwise peer keys so the device can recognize traffic for the host’s identity, including blind unicast, and authenticate it.
  • Acknowledgement delegation—for peers whose pairwise keys are provisioned, the device can send MAC acks on the host’s behalf while the host is away.
  • Saved state—the device can snapshot its configuration to non-volatile storage and resume autonomous operation after a power cycle with no host present.

There is no outbound queueing. A transmit either happens or fails while the host is attached to observe the result.

Identity Model

The full protocol supports exactly two node identities:

  • The device identity—a node belonging to the device itself, used for in-band management, diagnostics, repeater forwarding (see (#prop-mac-repeater-enabled)), and (in future revisions) periodic advertisement behavior. Its Ed25519 private key is held by the device and is never readable through this protocol.

    A device identity always exists. A device advertising CAP_DEV_IDENTITY that finds no stored keypair at power-on MUST generate one from a cryptographic random source and persist it before processing any host command; PROP_DEV_KEY therefore never reports an empty value on a running device. Provisioning an identity is not a commissioning step: a factory-fresh radio is already a node, and PROP_DEV_PRIVATE_KEY (see (#prop-dev-private-key)) exists to install a particular identity—restoring a known repeater onto replacement hardware—not to bring one into being.

    The corollary is that a radio holds a throwaway identity from first power-on until a specific one is installed. This is safe because it never reaches the air: PROP_PHY_ENABLED is false post-reset, and a radio with nothing saved boots with the PHY disabled. It is not safe automatically on the restore path—see (#cmd-restore).

  • The tethered host identity—the single UMSH identity owned by the attached host. Of the identity keypair itself, the device holds only the 32-byte public key; the host’s private key MUST NOT be transferred to the device, and this protocol provides no mechanism for doing so (see Security Boundary). The device may additionally hold host-domain state derived or delegated by the host— channel keys, per-peer symmetric keys, filters, and queued traffic—as defined in this chapter.

Because the device never holds the host’s private key, it cannot perform ECDH on the host’s behalf. All pairwise key material the device uses for the host identity is derived by the host and explicitly provisioned per peer (see (#prop-host-peer-keys)). The device identity is different: the device holds that private key, so it performs its own key agreement and needs only peer public keys (see (#prop-dev-peers)).

State Classes

Every piece of device state belongs to exactly one of three classes. The classes determine what survives a host attach, a change of host, and a power cycle.

Session State

State that exists only while a host is attached: transaction (TID) correlation, transport reassembly buffers, and the session-scoped properties PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL. Session state is reset to defaults on every attach. Resetting it never affects radio operation.

Device Domain

State that belongs to the device itself, independent of which host is attached:

  • the device identity keypair (independently persisted; never part of the saved snapshot—see (#saved-state))
  • the device identity’s channel keys ((#prop-dev-channel-keys)) and peer list ((#prop-dev-peers))
  • the RF configuration (PROP_PHY_*), including PROP_PHY_ENABLED, and the duty-cycle limit
  • the human-readable device name (PROP_DEV_NAME)
  • live battery telemetry (PROP_BATTERY), when CAP_BATTERY is present
  • device behavior settings (property identifiers 70–95): the repeater forwarding switch (PROP_MAC_REPEATER_ENABLED), with the rest of the range reserved for future definition (further repeater policy, positioning, periodic advertisement of the device identity, and similar)
  • transport configuration such as PROP_BLE_PAIRING_PIN

The RF configuration is deliberately device-domain: a site repeater keeps its frequency and regulatory limits no matter which phone pairs with it. An attached host may still reconfigure it at any time.

Host Domain

State that belongs to the currently configured tethered host identity:

  • PROP_HOST_KEY itself
  • the host’s channel keys and peer keys
  • the receive filter table and acknowledgement-delegation policy
  • the inbound queue: its configuration and its contents

The host domain is volatile across a power cycle, and only across a power cycle. It MUST NOT be persisted: it is not part of the saved snapshot, and at power-on every host-domain property takes its documented default.

It emphatically does survive a disconnect. A detached radio keeps filtering, queueing and acknowledging on behalf of its host for as long as it stays powered—that is the entire value of the host domain, and nothing about the host going out of range changes what the host wants done.

The two together give the host a simple rule with no detection in it: a host MUST establish its complete host domain on every tethered attach, writing every part of it rather than reasoning about what the device already holds. Key material cannot be compared anyway—the key tables never read it back (see (#provisioning-security)), so a peer’s pairwise keys can be replaced without changing anything the host can observe. Where the device is already provisioned as asked, the rewrite is redundant; that is preferable to depending on a signal that would also have to cover partial provisioning, another administrator’s intervention, and future device behavior. A boot generation or reset indication MAY be used to skip the rewrite as an optimization, never to decide whether it is needed.

Host Replacement

The host domain is keyed by PROP_HOST_KEY. Setting PROP_HOST_KEY to a value different from its current value—including setting it to empty —MUST atomically reset the entire host domain to defaults: the key tables and filter table are cleared, PROP_HOST_AUTO_ACK reverts to false, and the inbound queue is discarded. Because the host domain is never persisted, this is a live-state operation with no durable component: a power cycle cannot resurrect a previous host’s provisioning regardless.

Setting PROP_HOST_KEY to its current value is idempotent and has no side effects.

This rule is what makes re-pairing safe: when a companion radio is paired with a different phone, the new host configures its own identity and the previous host’s keys, filters, and queued traffic cease to exist—while the device domain (the radio’s own identity, channels, and settings) is untouched.

Attach, Detach, and Synchronization

How attach and detach are detected is defined by the transport binding:

  • BLE—enabling/disabling notifications on Frame Out, as specified in ULCP over BLE.
  • USB-CDC—assertion and deassertion of DTR on the ULCP interface.
  • Bare UART—implementation-defined. A device with no way to detect host presence MAY treat the host as permanently attached, in which case it never enters detached operation and offline assistance ((#inbound-queueing), (#ack-delegation)) is unavailable on that transport.

On attach, the device MUST reset session state (see (#state-classes)) and MUST NOT modify the device or host domains in any way. In particular, the PHY is not disabled and no property outside session state changes value. The device MUST NOT emit any frame before attach, and emits no unsolicited notification as a result of the attach itself.

Because attach no longer implies any known default state, the host synchronizes by fetching, not by assuming. The following post-attach procedure is RECOMMENDED:

  1. CMD_PROP_GET for PROP_LAST_STATUS. If it returns a reset code (see Reset Codes), the device has reset since the last host command, so any state that is not restored from saved state (notably queue contents) has been lost.
  2. CMD_PROP_GET for PROP_HOST_KEY. An empty value is the ordinary case after a power cycle—the host domain does not survive one—and the host simply provisions. A value matching the host’s own identity means its provisioning is still live from before the disconnect. Any other value means another host has taken the radio over since this host last attached; the queue and provisioning belong to that identity, and this host must decide whether to take the radio over (see (#host-replacement)) before doing anything else.
  3. CMD_PROP_GET for the device-domain properties the host depends on (PROP_SAVED, the PROP_PHY_* configuration), and for PROP_HOST_RX_QUEUE_COUNT.
  4. Establish the complete host domain (see (#host-domain)): write PROP_HOST_KEY, the key tables, the filter table and the delegation policy in full. The key tables are read back only to find entries the host no longer wants, which it removes; entries it does want are written whether or not the device reports them, since key material is not readable and so cannot be compared.
  5. Issue CMD_QUEUE_DRAIN when actually ready to process backlogged traffic.

More generally, a host MUST tolerate unsolicited CMD_PROP_IS, CMD_PROP_INSERTED, and CMD_PROP_REMOVED notifications at any time while attached, updating its view of the affected property accordingly: device state can change for reasons the host did not initiate, and publication of the new authoritative value is how the protocol reports that.

On detach, the device discards session state, keeps operating with the current device- and host-domain state, and begins detached operation: accepted frames are queued rather than delivered, and acknowledgement delegation (if enabled) becomes active.

Saved State

A device advertising CAP_SAVE can snapshot its provisioning to non-volatile storage so that it can operate autonomously across power cycles—the radio can be powered on in the morning with no phone present, restore its configuration, enable the PHY, and resume queueing and acknowledging on the host’s behalf.

  • CMD_SAVE (see (#cmd-save)) atomically writes the current device domain configuration—including the RF configuration and the current value of PROP_PHY_ENABLED—to non-volatile storage, replacing any previous snapshot.

    The host domain is never part of a snapshot (see (#host-domain)): a radio’s autonomy is its own configuration, and whichever host it is serving re-establishes its keys, filters and delegation policy on every attach. Dynamic read-only state, including queue contents and PROP_BATTERY, is likewise never saved. The device identity keypair is excluded for a different reason: it is independently persisted the moment it is installed or generated (see (#prop-dev-private-key)) and is changed only by explicit provisioning or CMD_CLEAR—neither CMD_RESTORE nor a reboot can revert the device identity to an earlier key.

  • At boot, if a snapshot exists, the device MUST restore it and resume operation accordingly before processing any host command: the RF configuration is applied and the PHY is re-enabled if it was enabled when saved, so a repeater is forwarding before anything else happens. Host-domain behavior—filtering, queueing, acknowledgement delegation— does not resume, because there is no host domain until a host provides one. If no snapshot exists, all properties take their documented post-reset values.

  • CMD_RESTORE (see (#cmd-restore)) reverts the device domain to the snapshot on demand, letting the host abort uncommitted configuration changes—without rebooting the hardware or dropping the ULCP link. It is observable either as a protocol reset (STATUS_RESET_RESTORED) or as a series of property-update publications; hosts handle both.

  • CMD_CLEAR (see (#cmd-clear)) erases the snapshot and all other persisted provisioning, including the device identity private key. It does not modify live (in-RAM) state; a subsequent CMD_RST completes a factory reset. Transport-level state such as BLE bonds is not affected.

  • PROP_SAVED (see (#prop-saved)) reports the state of the stored snapshot, which is not simply whether one exists—see (#snapshot-integrity).

Saving is explicit rather than automatic: nothing is written to non-volatile storage when properties change (the exceptions are the device identity and PROP_BLE_PAIRING_PIN). This gives the host control over flash wear and a well-defined “known good” configuration, and it means a radio never persists provisioning its host did not deliberately ask to keep.

Two consequences deserve emphasis:

  • Post-reset values come from the snapshot. CMD_RST reverts properties to their post-reset values, as always—but on a device with a snapshot, the post-reset value of every saved property is its saved value, not its documented default. This applies to the device domain only; the host domain has no saved value and always returns to its documented defaults. Factory defaults are restored by CMD_CLEAR followed by CMD_RST. A host that implements only the minimal protocol and expects documented defaults after CMD_RST will find the PHY already configured and enabled on a radio that was provisioned for autonomous operation; such a host still works if it explicitly sets the properties it cares about.
  • Queue contents and replay baselines are not saved. Frames queued before a power loss are gone afterward, even if they were acknowledged on the host’s behalf—the sender believes them delivered. Likewise the per-peer frame-counter baselines used by acknowledgement delegation restart (see Counter Resynchronization). These share the host domain’s lifetime, which is why re-provisioning after a power cycle is a resynchronization point rather than an inconvenience. Implementations MAY persist the queue to narrow this window, but hosts MUST NOT rely on it.

Snapshot Integrity

The snapshot is the one piece of state whose loss is silent and remote. A device configured to operate unattended comes back from a rejected snapshot deaf and non-forwarding, with nobody attached to be told, and recovery requires physically visiting it. The requirements below exist for that case.

  • A snapshot MUST be self-describing enough that a device can distinguish a payload it cannot read from an absent one. A device MUST NOT apply a payload it does not fully understand.
  • Devices MUST NOT silently boot bare after rejecting a snapshot. Where the storage retains earlier generations, the device MUST fall back to the newest generation that does decode, in preference to booting with documented defaults. A device MAY bound how far back it walks.
  • PROP_SAVED MUST report a fallback and an unreadable snapshot distinguishably from both “saved” and “nothing saved” (see (#prop-saved)). Devices with a local indicator SHOULD signal it there as well, since the host-visible report reaches nobody on an unattended device.
  • A device that restored an older generation is otherwise in normal operation: nothing is refused, and CMD_SAVE replaces the stored snapshot and clears the condition.

Only forward compatibility is required. Newer firmware MUST read snapshots written by older firmware, taking the documented default for anything the older writer did not record, and MUST ignore content it does not recognize. Firmware downgrade is out of scope: an older image reading a newer snapshot has no defined behavior, and saving from a downgraded image is destructive by design.

Additional Commands

The full protocol assigns the four command identifiers reserved by the minimal protocol for table operations, and adds four more:

IdMnemonicDirDescription
4CMD_PROP_INSERTHost->DeviceInsert an item into a multi-value property
5CMD_PROP_REMOVEHost->DeviceRemove an item from a multi-value property
7CMD_PROP_INSERTEDDevice->HostItem-inserted notification
8CMD_PROP_REMOVEDDevice->HostItem-removed notification
11CMD_QUEUE_DRAINHost->DeviceDeliver queued inbound frames
12CMD_SAVEHost->DeviceSave state to non-volatile storage
13CMD_CLEARHost->DeviceErase all saved state
14CMD_RESTOREHost->DeviceRestore state from the saved snapshot
15CMD_FACTORY_RESETHost->DeviceErase all mutable state (incl. bonds) and reboot

CMD 4: (Host -> Device) CMD_PROP_INSERT

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ITEM VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_INSERT

Insert item into property. Commands the device to add the given item to the given multi-value property, and to emit a CMD_PROP_INSERTED command for that property if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by exactly one item encoded in the property’s item form (see (#multi-value-properties)). The item is not preceded by a length prefix, regardless of whether the property uses item length prefixes in its multi-item value form; the framing layer bounds the item.

If the item is already present the command fails with STATUS_ALREADY, except where a property defines replacement semantics for matching items (see, e.g., (#prop-host-peer-keys)). If the property exists but is not a multi-value property, the command fails with STATUS_INVALID_ARGUMENT.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 5: (Host -> Device) CMD_PROP_REMOVE

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ITEM SELECTOR ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_REMOVE

Remove item from property. Commands the device to remove the item matching the given selector from the given multi-value property, and to emit a CMD_PROP_REMOVED command for that property if successful.

The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by an item selector. Each multi-value property documents its selector form; unless stated otherwise it is the full item value.

If no matching item is present, the command fails with STATUS_ITEM_NOT_FOUND. If the property exists but is not a multi-value property, the command fails with STATUS_INVALID_ARGUMENT.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 7: (Device -> Host) CMD_PROP_INSERTED

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_INSERTED

Item-inserted notification. Sent by the device in response to a successful CMD_PROP_INSERT (with the TID of that command), or unsolicited with a TID of zero when the device adds an item to a multi-value property for its own reasons.

The payload is the property identifier followed by the inserted item as the device reports it (see (#multi-value-properties))—never in a form containing key material.

CMD 8: (Device -> Host) CMD_PROP_REMOVED

  0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |      CMD      | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_PROP_REMOVED

Item-removed notification. Sent by the device in response to a successful CMD_PROP_REMOVE (with the TID of that command), or unsolicited with a TID of zero when the device removes an item from a multi-value property for its own reasons.

The payload is the property identifier followed by the removed item as the device reports it.

CMD 11: (Host -> Device) CMD_QUEUE_DRAIN

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |CMD_QUEUE_DRAIN|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_QUEUE_DRAIN

Deliver queued inbound frames. Commands the device to deliver every frame currently held in the inbound queue (see (#inbound-queueing)), oldest first, as ordinary CMD_STR_RECV commands on STR_PHY_RAW carrying the buffered-frame metadata described in (#buffered-metadata). The command payload SHOULD be empty and MUST be ignored.

Queued frames are only delivered in response to this command; attaching to the device does not by itself cause queued frames to be delivered (see (#inbound-queueing)). This lets the host finish synchronizing its session and signal that it is actually ready to process backlogged traffic.

The drain covers exactly the frames held in the queue when the command is received. Because accepted frames are always delivered live while a host is attached, the queue cannot grow while a drain is in progress: the drain always covers a fixed set of frames and always terminates. If the command was sent with a non-zero TID, the device reports completion by emitting CMD_PROP_IS for PROP_LAST_STATUS with STATUS_OK and the matching TID immediately after delivering the last covered frame. Draining an empty queue succeeds immediately.

Frames that arrive while a drain is in progress are not part of it: they are delivered live, and MAY therefore interleave with the buffered deliveries. RX_FLAG_BUFFERED distinguishes the two, and UMSH does not guarantee in-order delivery in any case (see (#inbound-queueing)).

If the device does not implement queueing (CAP_HOST_RX_QUEUE not advertised), the command fails with STATUS_UNIMPLEMENTED.

If an error occurs, the value of the emitted PROP_LAST_STATUS will be set accordingly to the status code for the error.

CMD 12: (Host -> Device) CMD_SAVE

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |    CMD_SAVE   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_SAVE

Save state. Commands the device to atomically write the current device domain to non-volatile storage as described in (#saved-state), replacing any existing snapshot. The command payload SHOULD be empty and MUST be ignored.

The response is a CMD_PROP_IS for PROP_LAST_STATUS with the command’s TID: STATUS_OK once the snapshot is durably stored, or an appropriate error status (for example STATUS_NOMEM) if it is not; on failure the previous snapshot, if any, MUST remain intact.

This command is only available on devices advertising CAP_SAVE; otherwise it fails with STATUS_UNIMPLEMENTED.

CMD 13: (Host -> Device) CMD_CLEAR

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |   CMD_CLEAR   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_CLEAR

Clear saved state. Commands the device to erase from non-volatile storage the saved snapshot and all other persisted provisioning, including the device identity private key. Live (in-RAM) state is unaffected; transport-level state such as BLE bonds and PROP_BLE_PAIRING_PIN is also unaffected. A CMD_CLEAR followed by CMD_RST restores factory protocol behavior.

Because a device identity always exists (see (#identity-model)), the CMD_RST that completes the sequence MUST generate and persist a new one rather than leave the device with none—the same thing a factory-fresh power-on does, and for the same reason. PROP_DEV_KEY therefore reports a different key after the sequence, never an empty one.

The previous identity is gone from the moment CMD_RST completes, but anything the device built around it—a running device node, in particular— MUST NOT continue to originate traffic under it, even where that state survives until the next boot.

The command payload SHOULD be empty and MUST be ignored. The response is a CMD_PROP_IS for PROP_LAST_STATUS with the command’s TID.

Unlike CMD_SAVE, this command is available regardless of capabilities; a device with nothing persisted succeeds trivially.

CMD 14: (Host -> Device) CMD_RESTORE

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |  CMD_RESTORE  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_RESTORE

Restore saved state. Commands the device to revert its device-domain configuration to the contents of the saved snapshot (see (#saved-state)). Regardless of how completion is reported (below), the resulting state is the same:

  • saved device-domain properties take their saved values, and the saved RF configuration and PHY enable state are applied;
  • the hardware is not reset, and the transport link and attach state are preserved;
  • the host domain is not touched: it is not part of a snapshot, so a restore has nothing to revert it to. The inbound queue contents, per-peer replay baselines, filters and delegation policy all survive unconditionally;
  • independently persisted state outside the snapshot—the device identity keypair and PROP_BLE_PAIRING_PIN—is not affected; and
  • the saved snapshot itself is not modified.

A restore never enables the PHY under an identity the snapshot was not taken for. A snapshot records which device identity was live when it was written. If that does not match the live PROP_DEV_KEY, the device MUST apply the restore with PROP_PHY_ENABLED false, whatever the snapshot says.

This is the replacement-hardware case, and it is the one path where a freshly generated identity can reach the air. Restoring a repeater’s saved domain onto a new board before installing that repeater’s key (see (#prop-dev-private-key)) would otherwise bring the radio up advertising as the node the snapshot describes, signing as a key nobody has ever seen. Installing the key first, then restoring, is the intended order and enables the PHY normally; the rule is what makes the wrong order safe rather than merely discouraged. A snapshot that does not record an identity is treated as matching.

Together with CMD_SAVE, this provides a commit/abort pattern: the host can make live configuration changes and either persist them with CMD_SAVE or discard them with CMD_RESTORE.

The command payload SHOULD be empty and SHOULD NOT be processed. A device reports a successful restore in one of two forms, both valid; the two forms differ only in reporting and in session-state handling, never in the resulting configuration or retained data:

  • Reset form—the device additionally resets its protocol session state (transaction bookkeeping and session-scoped properties), as on attach. As with CMD_RST, the TID is ignored; completion is signaled by an unsolicited CMD_PROP_IS for PROP_LAST_STATUS carrying the reset code STATUS_RESET_RESTORED (see (#full-reset-codes)). On receiving it, the host discards its cached view of all properties and assumes saved properties hold their saved values; dynamic read-only properties (such as PROP_HOST_RX_QUEUE_COUNT) reflect live state and are re-fetched.

  • Update form—the device applies the revert in place, emitting an unsolicited CMD_PROP_IS (with key material omitted, where applicable) for every property whose value changed, and then reports completion with CMD_PROP_IS for PROP_LAST_STATUS carrying STATUS_OK and the command’s TID. Session state is not reset in this form.

A host MUST handle both forms: it treats STATUS_RESET_RESTORED as full reversion to saved values, applies any unsolicited property updates, and recognizes completion by either the reset notification or the matching-TID STATUS_OK. This is not an extra burden in practice—hosts must already tolerate unsolicited CMD_PROP_IS value changes at any time (see (#attach-sync)). A host that does not know the snapshot’s contents (for example, because a previous session saved it) re-fetches the properties it depends on, exactly as in the post-attach procedure.

If an error occurs—in particular STATUS_INVALID_STATE when no snapshot exists (see PROP_SAVED)—the value of the emitted PROP_LAST_STATUS will be set accordingly, no state is modified, and no reset code is emitted.

CMD 15: (Host -> Device) CMD_FACTORY_RESET

 0                   1
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID |CMD_FACTORY_RST|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Figure: Structure of CMD_FACTORY_RESET

Return the radio to a blank factory state. Commands the device to erase every piece of mutable state it holds—both the persisted state CMD_CLEAR erases (the saved snapshot, all persisted provisioning, and the device identity private key) and the transport-level state CMD_CLEAR deliberately preserves: all BLE bonds and the configured PROP_BLE_PAIRING_PIN—and then reboot. After the reboot the radio is indistinguishable from one that has never been provisioned or paired.

This differs from CMD_CLEAR + CMD_RST in two ways: it also clears transport-level pairing state (bonds and PIN), and it performs a hardware reboot rather than only a protocol-session reset.

The command payload SHOULD be empty and MUST be ignored. Unlike every other command, CMD_FACTORY_RESET has no response: the device wipes its storage and reboots, which drops the transport link. A host treats the ensuing disconnect (and the radio’s subsequent reappearance in a factory state) as completion; it MUST NOT wait for a PROP_LAST_STATUS. The TID is therefore irrelevant.

Because clearing the bonds invalidates the encrypted link the command arrived on, a host that issues CMD_FACTORY_RESET over a bonded transport should also discard its own pairing to the radio.

This command is available regardless of capabilities.

Multi-Value Properties

A multi-value property holds an unordered set of items rather than a single value. The minimal protocol already contains one (PROP_CAPS, which is constant); the full protocol adds mutable ones.

The host writes items (CMD_PROP_SET, CMD_PROP_INSERT) in the property’s item form. When the device reports items (CMD_PROP_IS, CMD_PROP_INSERTED, CMD_PROP_REMOVED), it reports them exactly as written—except where the item form contains symmetric key material. Such a property documents what is reported instead: the entry with its key material omitted, or a short derived digest form (a channel key is reported as its derived channel identifier), so that secrets can never be read back (see (#provisioning-security)).

The commands valid on a mutable multi-value property are:

  • CMD_PROP_GET—the device replies with CMD_PROP_IS whose value is the concatenation of all items as reported. If the property is documented as having an item length prefix, each item is preceded by its length in octets encoded as a packed unsigned integer; properties whose reported items are fixed-size omit the prefix.
  • CMD_PROP_SET—replaces the entire contents with the items encoded in the value, each in item form (with the same length-prefix rule). Setting an empty value clears the property. Success is reported with a CMD_PROP_IS carrying the new complete value as reported.
  • CMD_PROP_INSERT / CMD_PROP_REMOVE—add or remove one item, as defined above.

Hosts manipulating large tables SHOULD prefer Insert/Remove over whole-table Set, since a full table may not fit comfortably in one frame on all transports.

Mutation Atomicity

State-changing operations in this protocol are transactional and fail closed:

  • The device MUST validate a complete request before changing any state. A whole-table CMD_PROP_SET whose value contains any invalid item fails without applying any of it.
  • Whole-table replacement is atomic: no observer of device behavior (frame filtering, acknowledgement decisions) sees a mixture of the old and new contents.
  • Operations that include durable writes—CMD_SAVE, CMD_CLEAR, installing or generating the device identity, and setting PROP_BLE_PAIRING_PIN—MUST NOT report success before the durable write has completed.
  • On any failure, the prior live and durable state remains unchanged, and the device MUST NOT emit CMD_PROP_IS, CMD_PROP_INSERTED, or CMD_PROP_REMOVED notifications describing a partially applied change.
  • Host replacement is atomic in the same sense: at no point may the device operate with a mixture of the old and new hosts’ keys, filters, or policy. It involves no durable write, so it cannot fail partway.

Atomicity is per operation, not per sequence. Establishing a host domain is several property writes, and an interruption between them leaves a mixture of old and new—bounded by the fact that a host-key change resets the domain first and a reboot empties it. A host repairs this the same way it provisions in the first place: by writing everything again.

Property Allocation

The full protocol allocates property identifiers by state class:

RangeClass
48–63Session-scoped and global protocol state
64–95Device domain
96–127Host domain (PROP_HOST_*)

Unassigned identifiers in these ranges are reserved.

IdMnemonicCommandsDescription
48PROP_MAC_PROMISCUOUSGet, SetDeliver all frames (session-scoped)
49PROP_SAVEDGetSaved-snapshot state
50PROP_MAC_BACKHAULGet, SetPoint-to-point link to the device’s node (session-scoped)
64PROP_DEV_KEYGetDevice identity public key
65PROP_DEV_PRIVATE_KEYSetDevice identity private key (write-only)
66PROP_DEV_CHANNEL_KEYSGet, Set, Insert, RemoveDevice identity channel keys
67PROP_DEV_PEERSGet, Set, Insert, RemoveDevice identity peer list
68PROP_DEV_NAMEGet, SetHuman-readable device name
69PROP_BATTERYGet, IsBattery status snapshot
70PROP_MAC_REPEATER_ENABLEDGet, SetAutonomous repeater forwarding enable
71PROP_IDENTGetSigned node identity of the device identity
72PROP_IDENT_ROLEGet, SetAdvertised node role, or empty to derive it
73PROP_IDENT_MOBILEGet, SetAdvertise the mobile capability bit
96PROP_HOST_KEYGet, SetTethered host identity public key
97PROP_HOST_CHANNEL_KEYSGet, Set, Insert, RemoveHost channel keys
98PROP_HOST_PEER_KEYSGet, Set, Insert, RemoveHost pairwise peer keys
99PROP_HOST_RX_FILTERSGet, Set, Insert, RemoveHost receive filter table
100PROP_HOST_AUTO_ACKGet, SetAcknowledgement delegation enable
101PROP_HOST_RX_QUEUE_COUNTGetFrames currently queued
102PROP_HOST_RX_QUEUE_CAPACITYGet, SetQueue capacity in frames
103PROP_HOST_RX_QUEUE_DROPPEDGetFrames dropped from the queue

PROP 48: PROP_MAC_PROMISCUOUS

  • Type: Single-Value, Read-Write, Session-Scoped
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Value Type: BOOL
  • Post-Attach Value: 0 (false)

When true, every frame the PHY successfully receives is delivered to the host over STR_PHY_RAW, bypassing receive filtering. This is a live-session diagnostic mode: frames that are delivered only because of promiscuous mode are never queued while the host is detached, and never acknowledged on the host’s behalf.

Session-scoped: it reverts to false on every attach.

PROP 49: PROP_SAVED

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_SAVE
  • Value Type: UINT8

Whether a saved snapshot is in effect (see (#saved-state))—that is, whether the device is armed for autonomous operation across a power cycle— and, when the answer is qualified, how:

ValueMeaning
0Nothing is saved. Every property holds its documented default.
1The most recently saved snapshot is in effect.
2A saved snapshot is in effect, but a newer stored generation was rejected at boot and this is an earlier one. The device is operating on configuration older than what was last saved.
3A snapshot exists but no stored generation could be read. The device booted with documented defaults despite having been saved.

Values 2 and 3 are conditions to report to the operator, not errors to recover from automatically: the configuration the device is running is not the configuration that was last written, and only whoever wrote it can say what should replace it. A successful CMD_SAVE returns the value to

  1. Values 2 and 3 persist for the remainder of the power cycle and MUST NOT be cleared by CMD_RST or CMD_RESTORE, neither of which re-reads storage.

A host that treats any non-zero value as “saved” behaves correctly, and loses only the warning.

PROP 50: PROP_MAC_BACKHAUL

  • Type: Single-Value, Read-Write, Session-Scoped
  • Asynchronous Updates: No
  • Required: CAP_MAC_BACKHAUL
  • Value Type: BOOL
  • Post-Attach Value: 0 (false)

When false, the host and the device’s own node are two listeners on one shared medium. Both transmit through the same radio and both hear what it receives, so a frame from one reaches the other only by way of some third node that repeats it.

When true, the host is instead a point-to-point neighbor of the device’s node:

  • A frame the host sends on STR_PHY_RAW is delivered to the node as though the node had heard it, and is never transmitted directly. It spends no airtime and is not subject to the duty-cycle limit.
  • Frames the node transmits are delivered to the host with RX_FLAG_SELF_TX set, subject to the usual receive filtering.
  • Frames the radio receives are not delivered to the host at all. The node is the only thing listening to the medium.

Traffic between the host and the mesh therefore crosses the device’s repeater: hop accounting, duplicate suppression, and forwarding policy are the node’s, applied to the host’s traffic as to anyone else’s. A device whose repeater is disabled still delivers the host’s frames to its own identities, but carries nothing onward.

PROP_MAC_PROMISCUOUS composes with this: it removes receive filtering from what the host is delivered, which in this mode is the node’s transmissions.

Session-scoped: it reverts to false on every attach.

PROP 64: PROP_DEV_KEY

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Persisted

The Ed25519 public key of the device identity (see (#identity-model)). The public key is also emitted as the success response when the private key is installed or generated (see (#prop-dev-private-key)).

An empty value means the device has no device identity. A conforming device does not report one in normal operation—an identity is generated at first boot if none is stored—so hosts SHOULD treat an empty value as a fault to surface rather than as an invitation to provision one.

Frames addressed to the device identity are processed by the device itself. They are additionally delivered or queued to the host only if they independently match the host’s receive filtering (see (#receive-filtering)).

PROP 65: PROP_DEV_PRIVATE_KEY

  • Type: Single-Value, Write-Only
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Persisted

Installs or generates the device identity private key. An identity always exists already (see (#identity-model)), so both forms replace one:

  • Setting a 32-octet value installs it as the device identity’s Ed25519 private key. This is the recovery path—moving a known repeater’s identity onto replacement hardware—not a commissioning step.
  • Setting an empty value commands the device to generate a fresh private key entirely on-device from its cryptographically secure random number generator. On-device generation is RECOMMENDED over installation, since a generated key never exists anywhere but the radio.

In both cases, success is reported by emitting CMD_PROP_IS for PROP_DEV_KEY—carrying the resulting public key—with the command’s TID. The private key itself is never emitted. Success MUST NOT be reported before the new identity is in effect and durably stored. Replacing an existing device identity is permitted; implementations SHOULD treat the device identity’s peer list and channel keys as still valid, since they are not derived from the identity key.

This property is write-only: CMD_PROP_GET MUST fail with STATUS_UNIMPLEMENTED and MUST NOT disclose the value or whether an identity is configured (use PROP_DEV_KEY for that).

The device identity is not part of the saved snapshot (see (#saved-state)): it is durably persisted as soon as it is installed or generated, and it is changed only by another set of this property or by CMD_CLEAR. CMD_RESTORE never reverts it—though it does read the identity a snapshot was taken under, and refuses to enable the PHY when it does not match (see (#cmd-restore)).

Replacing the identity takes effect for the property surface immediately and for anything the device built around the old key at the next boot. The old key stops being one the device claims at once, so a device running a device node MUST stop originating traffic under it rather than continue until the reboot.

Installing a private key is subject to the same transport security requirements as all key provisioning (see (#provisioning-security)).

PROP 66: PROP_DEV_CHANNEL_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Item Form: 32 octets (the channel key)
  • Digest Form: 2 octets (the derived channel identifier)
  • Remove Selector: the 32-octet channel key
  • Post-Reset Value: Empty, or restored from saved state

The set of channel keys belonging to the device identity—channels the radio’s own node participates in (for example, a site-infrastructure management channel). These are independent of the host domain: they survive host replacement and are distinct from PROP_HOST_CHANNEL_KEYS.

For each key the device derives the 2-byte channel identifier and the channel’s K_enc/K_mic (see Multicast Packet Keys). The digest form reported for each entry is that derived channel identifier; the key itself is never read back.

Device channel keys do not create implicit host receive filters: frames on these channels are consumed by the device node and reach the host only through the host’s own filtering.

PROP 67: PROP_DEV_PEERS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_DEV_IDENTITY
  • Item Form: 32 octets (the peer’s Ed25519 public key)
  • Remove Selector: the 32-octet public key
  • Post-Reset Value: Empty, or restored from saved state

The device identity’s peer list: the set of peer public keys the device node recognizes and may communicate with securely. Because the device holds the device identity’s private key, it performs its own key agreement (Unicast Key Agreement) for these peers—no symmetric keys are provisioned, and the entries contain no secret material.

How the device node uses this list (management access control, secure diagnostics, and so on) is application behavior outside the scope of this protocol.

PROP 68: PROP_DEV_NAME

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_DEV_NAME
  • Value Type: 1–64 octets of UTF-8, without U+0000
  • Post-Reset Value: Implementation-defined default, or restored from saved state

The operator-assigned, human-readable name of the physical device. It is independent of the device and host cryptographic identities and MUST NOT be derived from a bonded host or other host-domain state.

Setting the property changes the live name immediately. Like other ordinary device-domain configuration, it is included in a CMD_SAVE snapshot but is not independently persisted merely by being set. Applications and transports that present the device to a person SHOULD use this value when practical. They MAY shorten it to fit a constrained presentation, but MUST NOT split a UTF-8 code point when doing so.

The name is intentionally public metadata. Operators should assume that any value used in discovery advertisements can be observed by nearby devices.

PROP 69: PROP_BATTERY

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: Yes
  • Required: CAP_BATTERY
  • Value Type: Battery status snapshot (see below), or empty
  • Post-Reset Value: Current measurement, or empty if reporting is unsupported

A device advertising CAP_BATTERY has a battery capable of powering its operation and recognizes this property. The capability does not require the hardware to support reporting any measurement: an implementation that cannot report battery status at all answers CMD_PROP_GET successfully with an empty value.

A non-empty value is a snapshot of the battery measurements the platform supports, taken as one measurement event:

OctetsField
1Field flags
0 or 2Battery voltage, UINT16_LE, millivolts
0 or 1Battery level, UINT8, percent (0–100)
0+Charge state, PUI

Bits 0 (voltage), 1 (level), and 2 (charge state) of the field flags octet indicate which fields are present; present fields follow in the order above. Bits 3–7 are reserved and MUST be zero; a host MUST treat a value with a reserved bit set, or whose length does not match its field flags, as malformed.

Which fields a platform can report is fixed for a given hardware and firmware configuration; an individual snapshot carries those it can currently substantiate. A field is absent either because the implementation never reports that measurement, or because the value is not derivable in the device’s present state—a level estimated from resting terminal voltage is not obtainable while the pack is charging, and a charger that reports no completion signal offers no moment at which to recalibrate one. An implementation MUST NOT report a value it knows to be unreliable in place of omitting the field.

Absence MUST NOT be used to indicate a depleted or disconnected battery, and it is not how a failed measurement is reported: an implementation whose attempt to take a reading fails answers CMD_PROP_GET with STATUS_FAILURE.

A host MUST treat an absent field as unknown at that instant, and MUST NOT carry a value forward from an earlier snapshot in its place.

The value returned by CMD_PROP_GET reflects a measurement performed when the request is serviced, not a previously cached reading; concurrent requests MAY share one measurement. How each field is produced is platform-defined—in particular, the level estimate is not necessarily derived from the voltage measurement, and a platform with a fuel gauge may report a level without reporting a voltage at all.

The fields:

Battery voltage
The measured voltage at the battery terminals, in millivolts. This is the battery voltage, not an external-power input or regulated system voltage; it may therefore reflect the normal voltage elevation that occurs while the battery is charging.
Battery level
The implementation’s estimate of the battery’s state of charge, as an integer percentage from 0 through 100 inclusive. A host MUST NOT derive this value from the voltage field or assume that successive estimates change monotonically.
Charge state
The current battery charge state:
ValueName
0BATTERY_CHARGE_STATE_DISCHARGING
1BATTERY_CHARGE_STATE_CHARGING
2BATTERY_CHARGE_STATE_CHARGED
3BATTERY_CHARGE_STATE_NOT_CHARGING
BATTERY_CHARGE_STATE_DISCHARGING
External power is absent and the battery supplies the device. An absent field never carries that meaning.
BATTERY_CHARGE_STATE_CHARGING
The charging system reports that the battery is actively receiving charge.
BATTERY_CHARGE_STATE_CHARGED
External power is present and the implementation has evidence that charging completed. For a 4.2 V-class single-cell battery, measured voltage below 4.1 V is insufficient evidence of completion. A battery at 100 percent while operating without external power remains in BATTERY_CHARGE_STATE_DISCHARGING.
BATTERY_CHARGE_STATE_NOT_CHARGING
External power is detected, but the charging system does not report active charging and the implementation cannot substantiate charge completion. This state does not imply that the external source alone supplies the device.

The property contains live, read-only state. It is never included in a saved snapshot and is not changed by CMD_RESTORE. A device MAY emit unsolicited CMD_PROP_IS updates when the reported snapshot changes. Such updates SHOULD be coalesced or rate-limited so that measurement noise does not produce excessive ULCP traffic.

PROP 70: PROP_MAC_REPEATER_ENABLED

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_REPEATER
  • Value Type: BOOL
  • Post-Reset Value: Persisted

The first of the device-behavior settings (property identifiers 70–95). When true, the device identity acts as an autonomous mesh repeater: its on-board node forwards the routable UMSH frames it receives—off the air, from an IP multicast group, or over backhaul—according to Repeater Operation and Nodes With a LoRa Radio, and it sets the repeater capability bit in its node identity. When false, the device identity forwards nothing on any medium—it sends only its own traffic and its attached host’s—and the bit is clear.

The capability bit is a statement of fact and MUST track the live forwarding state. The advertised role is a separate matter: it is configuration, set through PROP_IDENT_ROLE (see (#prop-ident-role)), and defaults to being derived from this flag rather than being fixed by it. A mobile repeater and a fixed tracker are both expressible.

This property governs only the forwarding behavior of the device identity. It is independent of PROP_MAC_PROMISCUOUS (a session-scoped host-delivery mode) and of the host identity, which never forwards.

The flag is device-domain state: it is part of the saved snapshot, so a CMD_SAVE arms an unattended repeater across power cycles, and it survives a change of host.

Forwarding parameters other than the on/off switch—region codes, minimum RSSI/SNR, and flood-contention tuning—are not exposed by this property in the current protocol revision; a repeater applies its local defaults. Later revisions MAY define additional device-behavior properties (identifiers 70–95) to configure them.

PROP 71: PROP_IDENT

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: Signed node-identity payload

The device identity’s complete signed node identity: the canonical payload encoding—role, capabilities, and the descriptive options the device advertises— followed by its 64-octet detached EdDSA signature over that encoding.

This is the same statement the device makes over the air, in its standalone framing. A device MUST build it from the same values it would advertise in an Identity Request response, so a host reading it locally and a peer hearing it on the mesh cannot disagree about what the device is. It differs from that response in exactly two ways, both structural: it carries no request nonce, and it is authenticated by the signature rather than by an enclosing authenticated unicast.

The contents are nonce-free and timestamp-free, so the value is a function of the device’s configuration alone. A device MAY cache it, but is not required to: reading this property is an operator-scale event.

PROP 72: PROP_IDENT_ROLE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: UINT8, or empty
  • Post-Reset Value: Empty, or restored from saved state

The ROLE byte the device identity advertises (see Node Primary Role).

An empty value—the factory default—means the device derives the role from what it is actually doing: Repeater while PROP_MAC_REPEATER_ENABLED is set, Tracker otherwise. Any other value is advertised verbatim.

Role and forwarding are deliberately separate. Forwarding is a fact, reported through the repeater capability bit; the role is how the device presents itself, which is the operator’s choice. Deriving it by default keeps the common cases right without a configuration step, and setting it explicitly expresses the ones derivation cannot reach—a repeater that is also mobile, a fixed node that is not a repeater.

Tethering does not appear here, or anywhere in a node identity. Whether some host is currently attached over the local control link is a transient local relationship, not a durable characteristic of the node, and the mesh has no business knowing it.

PROP 73: PROP_IDENT_MOBILE

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_IDENT
  • Value Type: BOOL
  • Post-Reset Value: 0 (false), or restored from saved state

Whether the device identity advertises the mobile capability bit: true for a device that moves, false for one installed in a fixed location.

Orthogonal to PROP_IDENT_ROLE and to PROP_MAC_REPEATER_ENABLED, and orthogonal to whether a host is tethered. A hand-carried repeater and a pole-mounted sensor are both ordinary configurations.

PROP 96: PROP_HOST_KEY

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Value Type: 32 octets, or empty
  • Post-Reset Value: Empty

The Ed25519 public key of the tethered host identity. Setting this property tells the device which node identity it is assisting; an empty value means no host identity is configured.

Setting this property to a value different from its current value resets the entire host domain, as specified in (#host-replacement). Setting it to its current value is idempotent.

Like the rest of the host domain, this property is never saved: it is empty at every power-on, whatever the radio was doing before.

A configured host key acts as an implicit destination-hint receive filter (see (#receive-filtering)).

PROP 97: PROP_HOST_CHANNEL_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_KEYS
  • Item Form: 32 octets (the channel key)
  • Digest Form: 2 octets (the derived channel identifier)
  • Remove Selector: the 32-octet channel key
  • Post-Reset Value: Empty

The set of channel keys provisioned for the host identity. For each key the device derives the channel identifier and the channel K_enc/K_mic; the digest form is the derived channel identifier, and the key itself is never read back.

Each derived channel identifier acts as an implicit channel receive filter (see (#receive-filtering)). Host channel keys serve two assistance purposes:

  • recognizing multicast traffic on the host’s channels while the host is detached, so it can be queued; and
  • recognizing blind unicast traffic addressed to the host identity, which requires the channel key to decrypt the concealed destination/source addresses (see Blind Unicast Processing) and to form the combined blind unicast payload keys used for authentication and acknowledgement.

Channel keys are group-membership credentials, not host private keys, so provisioning them is consistent with the security boundary. They still grant whoever holds the device the ability to read and send traffic on those channels; see (#provisioning-security).

PROP 98: PROP_HOST_PEER_KEYS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: No
  • Asynchronous Updates: No
  • Required: CAP_HOST_KEYS
  • Item Form: Structure, 96 octets
  • Digest Form: 32 octets (the peer’s public key)
  • Remove Selector: the 32-octet peer public key
  • Post-Reset Value: Empty

Pairwise symmetric key material provisioned for specific already-known peers of the host identity. The item form is:

+---------------------+-----------+-----------+
| PEER_PUBLIC_KEY     |   K_ENC   |   K_MIC   |
+---------------------+-----------+-----------+
        32 B              32 B        32 B

Figure: Peer key entry item form

Where PEER_PUBLIC_KEY is the peer’s Ed25519 public key and K_ENC and K_MIC are the stable pairwise keys for the (host, peer) pair, derived by the host as described in HKDF Inputs for Unicast. The device never derives these itself—it cannot, because it does not hold the host’s private key.

As an exception to the usual CMD_PROP_INSERT duplicate rule, inserting an entry whose PEER_PUBLIC_KEY matches an existing entry replaces that entry. Replacement updates only the stored key material: the peer’s replay baseline (see (#ack-delegation)) and any frames already queued from that peer are unaffected, since both are keyed by the peer’s identity rather than by the key values. The digest form is the peer public key alone: K_ENC and K_MIC are never read back.

Provisioned peer keys let the device authenticate inbound unicast and blind unicast from those specific peers and acknowledge it on the host’s behalf (see (#ack-delegation)). They grant no capability regarding any other peer, and do not allow the device to establish new pairwise relationships.

PROP 99: PROP_HOST_RX_FILTERS

  • Type: Multiple-Value, Read-Write
  • Has Item Length Prefix: Yes
  • Asynchronous Updates: No
  • Required: CAP_HOST_FILTER
  • Item Form: Structure
  • Remove Selector: the full item
  • Post-Reset Value: Empty

The explicit receive filter table. Each item is a filter entry:

+-------------+----------------------+
| FILTER_TYPE | FILTER_VALUE ...
+-------------+----------------------+
     1 B          type-specific

Figure: Filter entry format

TypeNameValueMatches
0FILTER_DEST_HINT3 octetsFrames whose destination hint field equals the value
1FILTER_CHANNEL_ID2 octetsChannel-addressed frames (MCST, BUNI, BUAR) whose channel identifier equals the value
2FILTER_PKT_TYPE1 octetFrames whose FCF packet-type field equals the value (0–7)

Entries with an unrecognized FILTER_TYPE, or whose value length does not match the type, fail with STATUS_INVALID_ARGUMENT.

See (#receive-filtering) for how this table combines with the implicit filters derived from PROP_HOST_KEY and PROP_HOST_CHANNEL_KEYS.

PROP 100: PROP_HOST_AUTO_ACK

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_AUTO_ACK
  • Value Type: BOOL
  • Post-Reset Value: 0 (false)

When true, the device sends MAC acknowledgements on behalf of the host identity for qualifying frames received while the host is detached, as specified in (#ack-delegation). When false, the device never transmits on the host identity’s behalf.

PROP 101: PROP_HOST_RX_QUEUE_COUNT

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Value Type: UINT16_LE
  • Units: frames
  • Post-Reset Value: 0

The number of frames currently held in the inbound queue. The host typically reads this right after attaching to decide whether (and when) to issue CMD_QUEUE_DRAIN.

PROP 102: PROP_HOST_RX_QUEUE_CAPACITY

  • Type: Single-Value, Read-Write
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE (CMD_PROP_SET support is OPTIONAL)
  • Value Type: UINT16_LE
  • Units: frames
  • Post-Reset Value: Implementation-Specific

The maximum number of frames the inbound queue can hold. Devices with a fixed queue size fail CMD_PROP_SET with STATUS_UNIMPLEMENTED; devices that allow adjustment fail values they cannot honor with STATUS_INVALID_ARGUMENT.

PROP 103: PROP_HOST_RX_QUEUE_DROPPED

  • Type: Single-Value, Read-Only
  • Asynchronous Updates: No
  • Required: CAP_HOST_RX_QUEUE
  • Value Type: UINT32_LE
  • Units: frames
  • Post-Reset Value: 0

The cumulative number of frames discarded from the inbound queue—evicted by the circular queue-full policy or otherwise not retained (see (#inbound-queueing))—since the device last reset. A non-zero increase across a detached interval tells the host that its view of that interval is incomplete. The counter wraps modulo 2^32.

Receive Filtering

Receive filtering determines which successfully received frames are accepted for the host—delivered live when the host is attached, or queued when it is not.

The device evaluates each received frame against the union of:

  • the explicit filters in PROP_HOST_RX_FILTERS;
  • an implicit destination-hint filter for the first 3 bytes of PROP_HOST_KEY, when a host key is configured; and
  • an implicit channel filter for the derived channel identifier of each key in PROP_HOST_CHANNEL_KEYS.

A frame matching any filter is accepted. Hints and channel identifiers are prefilters, not proof (see Addressing); filtering by them can only over-accept, never mis-reject, and the host performs full cryptographic verification as usual.

The implicit destination-hint filter matches unicast traffic addressed to the host identity. Encrypted blind unicast addressed to the host is matched through its channel filter (its destination hint is concealed on the wire); the device MAY additionally use a provisioned channel key to decrypt the address block and narrow the match.

Two kinds of returning traffic identify themselves only by the trailer of a frame the host previously sent, so no destination or channel filter can address them: a MAC Ack carries no destination hint and its public ack_mic is the first 4 bytes of the acknowledged frame’s MIC, and a repeater’s onward copy of a host frame keeps the host’s trailer while any destination hint it carries names the remote peer. The device therefore records the leading 4 trailer bytes of each frame it transmits on the host’s behalf—for a MAC Ack, its ack_mic—and implicitly accepts any received frame whose trailer opens with a recorded value. For a MAC Ack the host sent, this matches the ack being carried onward; for any other frame, it matches both the returning acknowledgement and the frame being carried onward. The host’s forwarding-confirmation machinery must overhear every onward copy, acks included, to stop retransmitting. These records evict lazily, so multiple echoes of one send— acks arriving over different return routes, repeats from different repeaters—are all delivered. A MAC Ack whose ack_mic matches no recorded frame is still accepted if an explicit FILTER_PKT_TYPE entry selects it.

Broadcast packets—payload-carrying broadcasts and beacons alike—are implicitly accepted for live delivery: a broadcast is addressed to every node, the host included. The rule is live-only. While the host is detached, a broadcast is queued only when an explicit filter selects it (e.g., a FILTER_PKT_TYPE entry with value 0), so ambient broadcast traffic cannot displace queued unicast frames.

Device-domain state never creates implicit host filters: frames for the device identity or its channels reach the host only if the host’s own filtering matches them.

Compatibility rule: when no host key is configured, no host channel keys are provisioned, and the explicit filter table is empty, filtering is considered unconfigured and every successfully received frame is accepted. This is exactly the minimal protocol’s behavior, so a host that implements only the minimal protocol observes no difference on a full-protocol device in its factory state. As soon as any filter (implicit or explicit) exists, only matching frames are accepted.

Promiscuous mode (see (#prop-mac-promiscuous)) bypasses filtering for live delivery only.

Inbound Queueing

When CAP_HOST_RX_QUEUE is supported and the host is detached, accepted frames are placed in a FIFO inbound queue instead of being discarded. Each queue entry records the frame, its receive metadata (RSSI, LQI, SNR), the time of reception, and whether the device acknowledged it (see (#ack-delegation)).

When the host is attached, accepted frames are delivered live over STR_PHY_RAW exactly as in the minimal protocol. Attaching does not flush the queue: frames queued while the host was away remain queued until the host issues CMD_QUEUE_DRAIN (see (#cmd-queue-drain)). Frames received after attach are therefore delivered live even while older frames remain queued, and live deliveries MAY interleave with buffered deliveries during a drain (RX_FLAG_BUFFERED distinguishes them). A host that wants to process the backlog first drains promptly after attaching and MAY defer its processing of interleaved live deliveries; RX_AGE in the buffered-frame metadata gives coarse (one-second) relative timing but is not sufficient to reconstruct a strict total order—and UMSH itself does not guarantee in-order delivery in any case.

The queue is circular: when a new frame is accepted and the queue is full, the oldest queued frame is discarded and the new frame is appended. The queue therefore always holds the most recent accepted traffic. Every frame discarded by this eviction increments PROP_HOST_RX_QUEUE_DROPPED.

Eviction can discard a frame that was already acknowledged on the host’s behalf—the sender believes it delivered, but the host will never receive it. This is the same best-effort custody semantic that applies to power loss (see (#ack-delegation) and (#saved-state)): a delegated ack asserts volatile custody, not guaranteed delivery.

Duplicate detection for queueing uses the standard final-destination mechanisms of replay detection: per-peer frame-counter state and the recent accepted-MIC cache used for the backward window, where the device holds the keys to apply them. A frame identified as a previously accepted frame MUST NOT consume an additional queue slot; it is coalesced with the existing entry. A Route Retry form of a queued frame is the same logical packet (same MIC and frame counter) and coalesces with it. Coalescing a duplicate is separate from acknowledging it—a coalesced duplicate may still have its ack retransmitted under the duplicate-acknowledgement window (see (#ack-delegation)). For frames the device cannot authenticate (no provisioned keys), no protocol-defined duplicate detection applies and each received frame occupies its own entry.

Extended Recv Metadata

The Recv metadata of STR_PHY_RAW (see Metadata for Recv) may carry two further trailing fields:

  • RX_FLAGS (u8): Delivery flags
    • RX_FLAG_BUFFERED Bit 0: The frame was held in the inbound queue and is being delivered by CMD_QUEUE_DRAIN.
    • RX_FLAG_ACKED Bit 1: The device already transmitted a MAC ack for this frame on the host’s behalf. The host MUST NOT send another ack for it.
    • RX_FLAG_SELF_TX Bit 2: The device transmitted this frame itself and is delivering a copy of it. RX_RSSI and RX_SNR MUST carry their unsupported sentinels: a transmitter measures nothing.
    • All other bits: RESERVED, transmitted as zero
  • RX_AGE (u32, little-endian): Seconds elapsed between reception of the frame and its delivery to the host. Zero for live delivery.

As with the existing metadata fields, the metadata may be truncated at any field boundary; absent fields are treated as zero. A live delivery with nothing to flag MAY therefore omit these fields entirely, which keeps the encoding byte-compatible with the minimal protocol.

Acknowledgement Delegation

With PROP_HOST_AUTO_ACK enabled, the device acknowledges qualifying inbound frames so that senders’ retransmission logic is satisfied while the host is away. The device MUST transmit a MAC ack for a received frame if and only if all of the following hold:

  1. PROP_HOST_AUTO_ACK is true and no host is attached.
  2. The frame’s packet type requests acknowledgement: UNAR, or BUAR where the device also holds the frame’s channel key.
  3. The frame is addressed to the host identity: its (possibly decrypted) destination hint matches PROP_HOST_KEY, and its source resolves to an entry in PROP_HOST_PEER_KEYS—by full public key when the S flag is set, or by unique 3-byte prefix match otherwise.
  4. The frame authenticates: its MIC verifies under the pairwise K_MIC for UNAR, or under the combined blind unicast payload keys for BUAR.
  5. The frame is accepted as new by the replay-detection rules, applied per provisioned peer. The device advances a peer’s replay baseline only when it accepts a frame from that peer into the queue; a frame it fails to store leaves the baseline unchanged, so its retransmissions remain acceptable later.
  6. The frame was placed in the inbound queue (see (#inbound-queueing)). Because the queue is circular, placement normally succeeds by evicting the oldest entry when full; a frame that nevertheless cannot be stored (for example, one exceeding the device’s buffer) is not acknowledged, so the sender keeps retrying until the host returns.

Duplicates. An authenticated frame that replay detection identifies as a previously accepted frame—typically a retransmission whose original ack was lost—is not queued again, but the device MAY retransmit its acknowledgement under the core duplicate-acknowledgement window: only when the frame authenticates and its counter is no more than 8 behind the peer’s baseline, and without advancing or otherwise modifying the replay baseline. Re-acknowledging a duplicate is independent of queue coalescing (see (#inbound-queueing)) and does not mark anything newly accepted. Frames farther behind the baseline MUST NOT be acknowledged.

Reboot. Per-peer replay baselines are not saved (see (#saved-state)). After a device reset, the first authenticated frame accepted from a provisioned peer re-establishes that peer’s baseline at face value, exactly as on first contact (see Counter Resynchronization). The consequence is that after a reboot, previously captured authenticated frames may be accepted, queued, and acknowledged if replayed in a counter sequence acceptable from the newly established baseline. The host MAC remains authoritative for duplicate suppression when the frames are eventually delivered, so this creates a limited availability and resource-consumption window (queue slots and delegated acks), but it does not permit forgery or duplicate application delivery. Implementations concerned about this threat MAY persist a compact per-peer counter watermark (batched or range-reserved to limit flash wear), but hosts MUST NOT assume they do.

Custody. A delegated ack acknowledges volatile custody by default: the frame is held in RAM until drained, and the loss window on power failure is documented in (#saved-state). Implementations that persist the queue provide durable custody, but hosts and application protocols MUST NOT rely on it.

The acknowledgement is an ordinary MAC Ack packet: the ack MIC is the first 4 bytes of the acknowledged frame’s MIC and the 4-byte ack tag is computed as specified in Ack Tag Construction, using the provisioned pairwise keys (combined with the channel keys for BUAR). The ack carries no destination hint. If the original frame carried a flood hop count, the ack’s FHOPS_REM is initialized from the original frame’s FHOPS_ACC.

Delegated ack transmissions use the device’s normal transmit path and are subject to the configured duty-cycle limit; the device MUST NOT exceed the limit to send an ack. An ack that cannot be sent leaves the queued frame marked unacknowledged.

Frames that are accepted but fail any of conditions 2–5—no peer key, no channel key, authentication impossible to evaluate—are still queued (subject to filtering); they are simply not acknowledged. The host performs its own verification after draining and may ack late if the application finds that useful.

While a host is attached, the device never acks on its behalf: live-delivered frames are the host’s responsibility. Acks generated by the device identity for its own traffic are ordinary device-node behavior and are not governed by this section.

Provisioning Security

Provisioning moves real key material onto the device, within the limits of the security boundary: channel keys and per-peer symmetric keys—and the device identity’s own private key— but never the host’s private key. The rules:

  • All symmetric key material, and the device identity private key, is write-only. CMD_PROP_GET and all device-emitted notifications report key-bearing properties without their secrets (see (#multi-value-properties)): peer public keys without K_ENC/K_MIC, derived channel identifiers (the digest form) instead of channel keys, and never the device private key. This holds for both identities’ key tables. These read-backs let the host verify what is provisioned after a reconnect without any secret ever crossing the link a second time— which matters because more than one host may be able to attach over the radio’s lifetime (transport bonds are possession credentials, not identity credentials), and a later host must not be able to extract an earlier host’s keys.
  • Commands that carry key material—CMD_PROP_SET and CMD_PROP_INSERT for the key tables, and any set of PROP_DEV_PRIVATE_KEY—MUST NOT be carried over a transport that does not meet the requirements of the transport’s security binding: physical possession for serial transports, or an encrypted bonded LESC link as specified in ULCP over BLE.
  • A compromised or stolen device exposes the provisioned channels, the provisioned pairwise conversations, and its own device identity, but cannot impersonate the host to any new peer, cannot sign as the host, and cannot decrypt traffic for peers or channels that were never provisioned.
  • Hosts SHOULD provision the minimum useful set of peers and channels, SHOULD remove entries that are no longer needed, and SHOULD prefer on-device generation of the device identity over installing one.
  • A device advertising CAP_SAVE MUST store persisted key material in the most protected storage available to it.

Additional Status Codes

The full protocol assigns two additional status codes (see Status Codes):

IdName
19STATUS_ALREADY
20STATUS_ITEM_NOT_FOUND
STATUS_ALREADY
The requested state is already in effect; in particular, the item passed to CMD_PROP_INSERT is already present in the property.
STATUS_ITEM_NOT_FOUND
The item or selector passed to CMD_PROP_REMOVE does not match any item in the property.

Additional Reset Codes

The full protocol assigns one additional reset code (see Reset Codes):

IdName
115STATUS_RESET_RESTORED
STATUS_RESET_RESTORED
Protocol reset into the saved snapshot, emitted when a device completes CMD_RESTORE in its reset form (see (#cmd-restore)). Unlike the other reset codes, this one does not indicate a hardware or firmware restart: the transport link and attach state survive it. Like STATUS_RESET_SOFTWARE, it is emitted during normal operation and does not indicate a problem.

Additional Capabilities

The full protocol assigns the following capability codes (see Capabilities):

CodeNameRequiresGrants
32CAP_HOST_FILTER—PROP_HOST_KEY, PROP_MAC_PROMISCUOUS, PROP_HOST_RX_FILTERS, and the receive-filtering behavior
33CAP_HOST_RX_QUEUECAP_HOST_FILTERThe inbound queue, its properties, CMD_QUEUE_DRAIN, and the buffered-frame metadata
34CAP_HOST_KEYSCAP_HOST_FILTERPROP_HOST_CHANNEL_KEYS and PROP_HOST_PEER_KEYS
35CAP_HOST_AUTO_ACKCAP_HOST_KEYS, CAP_HOST_RX_QUEUEPROP_HOST_AUTO_ACK and acknowledgement delegation
36CAP_SAVE—CMD_SAVE, CMD_RESTORE, PROP_SAVED, and boot-time restoration of saved state
37CAP_DEV_IDENTITY—The device identity: PROP_DEV_KEY, PROP_DEV_PRIVATE_KEY, PROP_DEV_CHANNEL_KEYS, PROP_DEV_PEERS
38CAP_DEV_NAME—PROP_DEV_NAME
39CAP_BATTERY—Battery-powered operation and PROP_BATTERY
40CAP_REPEATERCAP_DEV_IDENTITYPROP_MAC_REPEATER_ENABLED and autonomous repeater forwarding by the device identity
41CAP_IDENTCAP_DEV_IDENTITYPROP_IDENT, PROP_IDENT_ROLE, PROP_IDENT_MOBILE—serving and configuring the device identity’s advertised node identity

A device MUST NOT advertise a capability without also advertising the capabilities it requires. CMD_PROP_INSERT/CMD_PROP_REMOVE, CMD_CLEAR, and the two additional status codes are part of the base protocol and need no capability; a device that defines no mutable multi-value properties simply has nothing to apply them to.

Identity Export Format

Note

This section is an early work in progress and this format may change significantly.

This appendix defines a portable, passphrase-protected artifact for backing up and restoring a node identity, together with the secret material and local knowledge that make a restored identity immediately useful. It is produced and consumed by host implementations—phones, tablets, and desktops. A ULCP device never generates, stores, or parses an export artifact, and a device’s own identity is never exported through this format.

The artifact is a single binary object with two layers:

  • an envelope that binds a format version and key-derivation parameters to an encrypted, authenticated body; and
  • a payload inside the envelope: a CBOR map carrying the identity secret, counter-recovery information, and optional local state.

The same format serves two profiles distinguished only by which payload sections are present:

  • a core export carries only the identity and counter sections. It is small enough to render as a short QR sequence (see QR Part Framing) and suits printed or engraved recovery copies.
  • a full export additionally carries channel keys, contacts, and application settings. It is intended for file storage.

No umsh: URI form is defined for export artifacts. This is deliberate: private key material must not flow through link handlers, pasteboards, or URI-preview machinery. The recommended file extension is .umshid.

Envelope

All multi-byte integers are big-endian, as elsewhere in UMSH.

OffsetSizeFieldValue
06MagicASCII UMSHID
61Format version0x01
71KDF identifier0x01 = Argon2id
84Argon2id memory cost mKiB
124Argon2id time cost tpasses
161Argon2id parallelism planes
1716KDF saltrandom per export
331Cipher identifier0x01 = AES-SIV (RFC 5297) with AES-256
3416MICfull 16-byte S2V output V
50—Ciphertextencrypted payload

Bytes 0–33 (everything before the MIC) form the envelope header. The header is authenticated as associated data; any modification of the version, KDF parameters, salt, or cipher identifier invalidates the MIC.

The KDF salt MUST be freshly generated from a cryptographically secure random source for every export, including re-exports of the same identity.

Unrecognized format version, KDF identifier, or cipher identifier values MUST cause the importer to reject the artifact before attempting key derivation.

Key Derivation

The passphrase is encoded as UTF-8 after Unicode NFC normalization. Normalization is required: text input methods on different platforms produce different codepoint sequences for visually identical passphrases, and a cross-platform artifact must decrypt identically everywhere.

prk  = Argon2id(passphrase, kdf_salt, m, t, p, taglen = 32)

ikm  = prk
salt = "UMSH-IDEXPORT-SALT"
info = "UMSH-IDEXPORT-V2"
okm  = HKDF-SHA256(ikm, salt, info, 64)

K_mic = okm[0..32]
K_enc = okm[32..64]

Exporters MUST use at least m = 19456 KiB, t = 2, p = 1, and SHOULD use m = 65536 KiB, t = 3, p = 1 where device memory allows. Importers MUST honor the parameters carried in the header, but MAY refuse artifacts whose parameters exceed a local resource ceiling (for example m > 1048576 KiB, t > 32, or p > 4) to avoid resource-exhaustion attacks through crafted headers.

Encryption and Authentication

The payload is protected with the same construction used for packets (see Encrypted Packets), with the full 16-byte MIC and the envelope header as associated data:

  1. Compute the full 16-byte synthetic IV V = S2V(K_mic, S1, S2) per RFC 5297 §2.4, where S1 is the envelope header and S2 is the payload plaintext. V is stored as the MIC.
  2. Form the CTR IV by clearing the top bit of bytes 8 and 12 of V (RFC 5297 §2.6).
  3. Encrypt the payload using AES-256-CTR with K_enc and that IV.

Because the MIC is always full-length here, this is exactly AEAD_AES_SIV_CMAC_512 (RFC 5297) with key K = K_mic || K_enc and the envelope header as the single associated-data component.

To import, derive the keys, decrypt the ciphertext, recompute V over the header and recovered plaintext, and compare it to the stored MIC in constant time. On mismatch the importer MUST report a single generic failure: a wrong passphrase and a corrupted or forged artifact are deliberately indistinguishable.

Payload

The payload is a CBOR (RFC 8949) map using unsigned-integer keys. Definite-length encoding MUST be used. Deterministic encoding is not required; the envelope, not the payload encoding, provides integrity.

KeySectionTypePresence
1Identitymaprequired
2Countersmaprequired
3Channelsarray of mapsoptional
4Contactsarray of mapsoptional
5Application settingsmapoptional

Importers MUST ignore unrecognized top-level keys and unrecognized keys within any section. Unrecognized content MUST NOT be preserved into a later re-export: a section defined by a future version may carry security state that must not outlive the format revision that understands it.

Identity Section

KeyFieldTypePresence
1Private key seed32-byte byte stringrequired
2Createdunsigned (Unix seconds)optional
3Display nametext stringoptional
4Advertisementbyte stringoptional

The seed is the node’s Ed25519 private key seed, from which the Ed25519 public key and the derived X25519 key are obtained. The public key is not stored; the importer MUST derive it from the seed, and that derived key is the restored identity’s address.

The advertisement, when present, is the node’s most recent advertisement in canonical wire form. The importer SHOULD verify its signature against the derived public key and discard it—without failing the restore—if verification fails.

Counter Section

KeyFieldTypePresence
1TX counter floorunsignedrequired
2Restore generationunsignedrequired

The TX counter floor is the highest outbound frame counter value the exporting implementation knows to have been used, maximized across however many outbound counters it tracks. It is a floor, not a current value: the exporting device may continue transmitting after the export is created, so the artifact is stale by an unknown amount the moment it exists. Restore safety comes from the advance rule below, not from the accuracy of this field.

The restore generation records how many times this identity had been restored from an export when the artifact was created. It begins at zero, and each successful restore records one more than the value found in the artifact. It is bookkeeping for diagnostics and future exports; it is not a security mechanism.

Channel Section

Each entry describes one channel membership:

KeyFieldTypePresence
1Channel key32-byte byte stringrequired
2Kindunsignedrequired
3Nametext stringoptional

Kind values: 1 = private channel, 2 = named channel, 3 = managed channel. For named channels the key is derivable from the canonical name, but the key is stored regardless so that import never depends on name canonicalization. The name field carries the canonical name for named channels and the local display name otherwise.

Contact Section

Each entry describes one known peer:

KeyFieldTypePresence
1Public key32-byte byte stringrequired
2Local aliastext stringoptional
3Advertisementbyte stringoptional

The advertisement, when present, is the peer’s cached advertisement in canonical wire form. The importer SHOULD verify its signature against the entry’s public key and discard it on failure. Local aliases are display state, not protocol state.

Ephemeral session state is never exported: PFS sessions are local to the device that negotiated them, and receive-side replay baselines are re-established through the normal first-contact and re-baselining rules.

Application Settings Section

A map with text-string keys and arbitrary CBOR values, namespaced by the producing application (for example ios.notifications.previews). Contents are application-defined and restored best-effort; importers MUST ignore entries they do not understand. This section MUST NOT contain key material or any other secret—secrets belong only in the sections defined above.

Restore Procedure

  1. Parse the envelope; reject unknown version, KDF, or cipher identifiers.

  2. Derive keys from the passphrase and header parameters; decrypt and authenticate. Report authentication failure generically.

  3. Derive the public key from the seed and present the identity (name, complete address) for explicit confirmation before committing anything.

  4. On confirmation, compute the restored outbound frame counter:

    block    = 2^24
    restored = (floor(tx_floor / block) + 2) * block
    

    If restored does not fit in the 4-byte counter space, the restore MUST fail; the identity’s counter space is effectively exhausted and the identity should be retired rather than restored.

  5. Persist the identity, the restored counter value, and the incremented restore generation before the identity sends any authenticated traffic.

  6. Import optional sections, verifying signatures where specified.

The advance rule skips at least one full block of 2²⁴ counter values beyond the recorded floor. This dominates any plausible transmission volume between export and restore on a LoRa-class link, and bounds the identity to roughly 250 restores across its lifetime—a deliberate trade of counter space for safety against a stale floor.

Restoring an identity does not revoke the source: the exporting device, and every other copy of the artifact, still holds a working private key. The restore flow MUST state that the exporting device is to stop using the identity; concurrent use violates the monotonic-counter assumption that peers rely on for replay protection. After a successful restore, implementations SHOULD encourage creating a fresh export, since existing artifacts remain valid but describe a stale counter floor and generation.

QR Part Framing

An artifact rendered as QR codes is split into parts, each carried in one symbol using QR byte mode:

OffsetSizeFieldValue
04Part magicASCII UMQR
41Framing version0x01
54Artifact checkfirst 4 bytes of SHA-256 of the complete envelope
91Part index0-based
101Part counttotal parts, ≥ 1
11—Chunkenvelope bytes

Concatenating the chunks in index order reproduces the envelope. An importer MUST NOT combine parts whose artifact check values differ, and MUST verify the reassembled envelope against the check value before attempting decryption. The check value is a reassembly guard against mixing parts from different exports; the envelope MIC remains the integrity mechanism.

A core export fits in two to three modest QR symbols. Full exports are not intended for QR presentation.

Security Considerations

The passphrase is the floor. Argon2id raises the cost of guessing but cannot rescue a weak passphrase, and the artifact is exposed to offline attack wherever it is stored. Applications should communicate this when the passphrase is chosen.

A full export is more than a key. Channel keys are membership credentials; an attacker who decrypts a full export can read and send on every included channel, not merely impersonate the identity. Applications should present a full export as at least as sensitive as the identity itself.

Artifacts cannot be revoked. Every copy of an export remains a valid credential for as long as the identity and the included channel keys remain in use. Deleting the file an application knows about does not delete copies. Retiring a compromised export means retiring the identity and rotating the included channel keys.

Decrypted material must be handled like the live key. Implementations zeroize decrypted payload buffers, never log payload contents, and never expose the seed or channel keys outside the component that consumes them.

Header parameters are attacker-controlled until authenticated. The KDF parameters are read before any authentication is possible; the resource ceilings in Key Derivation exist so a crafted header cannot demand unbounded memory or time.

Internet Bridging

Note

This section is an early work in progress and this protocol may change significantly.

This appendix defines a client–server realization of the bridge described in Routing Overview § Bridging: a set of radios in different places, joined by an authenticated tunnel over a reliable stream transport such as the internet. It specifies the tunnel wire protocol, how a participant attaches its radio, and the small number of decisions the tunnel itself makes about what it carries.

Caution

The cautions in Routing Overview § Bridging apply in full: internet bridges cannot be relied upon in an emergency and can waste airtime with non-local chatter. This appendix exists so that bridges which are deployed anyway behave predictably and conservatively.

Model

A bridge consists of one bridge server and one or more bridge clients. Together they form a hidden radio layer: a medium that joins segments which are nowhere near each other.

  • Each participant fronts a ULCP device of its own, attached as a tethered host in backhaul mode. The host is therefore not on the shared medium at all—it is a point-to-point neighbor of the device’s own node.
  • The server’s interfaces are its own radio, when it has one, plus one interface per connected client, plus any host interfaces it offers, plus its LAN interface, when it has one. The server’s radio is a participant like any other; nothing distinguishes it but the absence of a tunnel.
  • The server copies each frame it receives to its other interfaces. It makes no forwarding decisions and holds no mesh state.

The bridge has no identity on the mesh. It appears in no route, answers to no address, and originates no traffic. The nodes that carry bridged traffic are the ones behind the participants’ radios—and, where a host interface is offered, the host on the far side of it—each holding its own identity.

A Crossing Is Two Repeater Hops

Because every participant is a point-to-point neighbor of its device’s node, the node’s repeater is the bridge’s forwarding policy—hop accounting, duplicate suppression, region and signal policy, all applied to bridged traffic exactly as to anything else the node hears.

A packet crosses in four steps:

  1. A node on the ingress segment transmits. The participant’s device hears it off the air and its node forwards it by the ordinary forwarding procedure: duplicate check, flood hop accounting, trace prepend with a real signal measurement, and a transmission on that same segment.
  2. That transmission is delivered to the attached host, which writes it to the tunnel. What crosses is the ingress node’s output, already rewritten—not the frame as it was first heard.
  3. The server copies it to its other interfaces.
  4. Each receiving participant hands the frame to its own device’s node, which receives it as a packet carrying no measurements and forwards it by the same procedure: a second duplicate check, a second hop accounting, a trace record whose zero signal bytes say that nothing was measured, and no contention window, since nobody else heard it.

Two consequences follow, and deployments should plan for both. A crossing spends two flood hops rather than one, so a packet sent with a budget of three arrives on the far segment with one. And the ingress node’s transmission in step 1 is an ordinary repeat on the segment the packet came from, which is exactly what the previous hop listens for: the bridge satisfies forwarding confirmation without transmitting anything for the purpose.

Everything the node transmits reaches its host, so everything the node transmits crosses: its repeats, its beacons, its acknowledgements, and its own application traffic. That is the node participating in the mesh the bridge has joined it to.

Tunnel Echoes

A frame transmitted by an egress node in step 4 is delivered to that node’s own host in the same way, written to the tunnel, and copied onward. Each of the other participants’ nodes then receives a packet it has already forwarded, and its duplicate cache ends it there.

This costs one extra round of tunnel messages per crossing and no airtime at all. It is what makes the arrangement safe: the duplicate caches at the participants, not any rule in the bridge, are what stop a frame circulating. A deployment MUST NOT attach a participant whose device does not suppress duplicates.

Radio configuration is local to each participant: a client configures its own device, and the tunnel provides no mechanism for managing a remote participant’s radio.

Tunnel Transport

Connection and Authentication

The tunnel is a TLS 1.3 connection over TCP; the client connects to the server. Earlier TLS versions MUST NOT be negotiated.

Peer authentication uses either external pre-shared keys (TLS 1.3 PSK handshakes) or mutually authenticated certificates with pinned or locally trusted roots. Each client SHOULD hold a distinct credential: it is how the server identifies a client for policy and rate limiting, and it allows one client to be revoked without re-keying the rest.

A deployment MAY use an Ed25519 node identity as its certificate key, each side pinning the peer’s public key rather than a certificate. The pinned key MUST then be held against the TLS 1.3 CertificateVerify signature—proof that the peer possesses the identity, independent of anything the certificate claims—rather than against the certificate’s contents. This gives every tunnel credential a UMSH address, so a participant’s credential can later serve as a mesh-addressable identity for management without re-keying.

The tunnel carries no version of its own. Participants SHOULD offer the ALPN protocol identifier umsh-bridge/1, so that an incompatible future revision fails the handshake instead of misparsing frames.

A participant MUST NOT send a TLS record carrying more than 4096 octets of plaintext. This bounds the record buffer a constrained participant needs; a participant that cannot buffer a larger record closes the connection.

Message Framing

The tunnel is a stream of HDLC-Lite frames, exactly as ULCP uses on asynchronous serial links (see Framing and Common Semantics). The frame check sequence is redundant beneath TLS but is retained so implementations can reuse their existing framing code unchanged.

There is no message header. A non-empty frame contains one STR_PHY_RAW structure and nothing else; an empty frame is a keepalive. Because the payload is exactly the ULCP stream structure, a participant relays bytes without parsing them:

  • Client to server: something the client’s node transmitted—the body of the CMD_STR_RECV that delivered it, written unmodified, RX_FLAG_SELF_TX and all.
  • Server to client: a frame to hand to the client’s node, passed as the body of a CMD_STR_SEND on STR_PHY_RAW.

The metadata a frame carries across the tunnel describes how it was received, and a transmit request needs metadata of its own. The participant that hands a frame to its device therefore MUST replace the accompanying metadata with transmit metadata, and SHOULD do so only at that point, so that the received metadata—including any buffered-frame age—remains available to the staleness rule below for as long as the frame is in flight.

A frame that is malformed, or larger than the receiving participant is prepared to transmit, is discarded.

Keepalive and Reconnection

A participant writes a bare flag octet (0x7E) whenever it has sent nothing for a keepalive interval—10 seconds is a reasonable default— and closes the connection once it has received nothing for an idle timeout, by default 30 seconds. The two directions are independent.

Empty frames are the idle fill of HDLC-Lite and a conforming decoder already discards them, so the keepalive needs no message of its own. Two properties make it sufficient. Liveness is measured in received octets rather than decoded messages, so a discarded flag still counts as activity. And because the keepalive is written by the participant’s own relay logic, a peer whose relay has wedged stops emitting it even while its TLS connection stays open.

A stalled local transmit path is not visible to the idle timer, since the peer keeps talking. It appears as tunnel-queue backpressure instead, which is bounded below.

Frames queued for a tunnel that fails or backs up are stale by definition: participants MUST bound their tunnel queues, SHOULD drop the oldest frames first under backpressure, and MUST discard queued frames when a connection is re-established rather than flushing them into the new session.

Staleness is enforced by the sender, and no age accompanies a frame on the wire. A participant SHOULD discard a frame rather than write it once the frame is older than a configured limit—ten seconds is a reasonable default—counting any device-side queueing reported through buffered-frame metadata. A frame that old describes a mesh that has moved on.

Radio Attachment

Each participant attaches to its device as an ordinary tethered host, without resetting it, and sets PROP_MAC_BACKHAUL to true. This requires the device to advertise CAP_MAC_BACKHAUL.

A device that does not advertise CAP_MAC_BACKHAUL MUST NOT be attached to a bridge on the shared medium instead. A host on the medium transmits directly: its frames reach the air without passing any node’s duplicate suppression, hop accounting, or forwarding policy, which are the whole of what makes a crossing safe.

A participant SHOULD also set PROP_MAC_PROMISCUOUS to true. In backhaul mode this widens what the host is delivered from those of the node’s transmissions that pass its receive filtering to all of them— which for a device with a provisioned host domain is the difference between carrying the node’s repeats and silently dropping them. A device with no host domain provisioned filters nothing, so this is a safeguard rather than a requirement.

Both properties are session-scoped and revert on every attach, so both must be re-asserted after each reconnection to the device.

A participant MUST NOT write PROP_MAC_REPEATER_ENABLED. Whether a device repeats is persisted, device-domain configuration belonging to whoever provisioned it. A participant whose device has its repeater disabled is a leaf: its node is reachable across the bridge and its own traffic crosses, but nothing is carried onward from its segment, and nothing arriving from the bridge reaches the air. This is a legitimate deployment, and an implementation SHOULD report it rather than treat it as a fault.

Transmission requires the device to advertise CAP_WRITABLE_RAW_STREAM. Participants SHOULD use confirmed transmissions (non-zero TID) so that refusals are observed rather than silent, and bridged transmissions MUST NOT set TX_FLAG_NODUTY: the device’s duty-cycle enforcement is the backstop against a bridge that would otherwise consume a segment’s airtime budget.

A backhauled hand-off crosses a wire. It contends for no channel, spends no airtime, and is not charged against the duty limit—the airtime is spent later, and accounted to the node, if the node decides to transmit. What a hand-off can meet is a node whose receive queue is full, which is reported as STATUS_CCA_FAILURE for want of a better code. A participant SHOULD wait briefly and offer the frame again, and MUST continue draining its device’s receive path while it waits: draining the node’s output is what makes room in its input.

Whether a handed-off frame reaches the air is the node’s decision. A duplicate it has already forwarded, or one whose flood budget is spent, ends at the node. That is the forwarding policy working, and a participant MUST NOT treat it as a failed hand-off or retry it.

No participant provisions node logic for the bridge: there is no bridge identity to provision. A participant’s device is configured as whatever node its owner intends it to be.

Integrated Clients

A device may implement the client internally, configured through Bridge Client. Its internal backhaul connects directly to its own node, independently of any tethered host. The attachment delivers only the node’s successful transmissions to the tunnel, including its self-transmit metadata. It hands tunnel input only to that node as an unmeasured backhaul reception. Overheard radio input and a tethered host’s direct transmissions are not independently copied to the tunnel.

This is the same two-repeater-hop crossing as an external participant. The internal client does not change the attached host’s backhaul or promiscuous settings, does not enable the repeater, and does not bypass its forwarding policy. Its TLS authentication key is the device identity. The server authorizes the public identity exposed by PROP_DEV_KEY.

The internal attachment uses the same STR_PHY_RAW wire representation as external clients: a little-endian UINT16 packet length, packet bytes, then receive metadata. The integrated client preserves HDLC-Lite framing, TLS 1.3, and the umsh-bridge/1 ALPN offer. Its eight-frame queues discard the oldest frame under pressure and expire queued frames after ten seconds. A connection boundary discards both queues and the partial HDLC decoder state.

A device may run an integrated client and be in an IP multicast group at once. Both receive every frame the radio transmits, and both hand their frames to the node as receptions, so the node’s repeater is the one forwarding decision between them: a frame from either crosses to the other by being repeated on the radio. With its repeater disabled the node carries nothing between them; its own traffic still reaches both.

Host Interfaces

A server MAY offer host interfaces: interfaces whose far end is not a participant fronting a segment but a single host, attached directly to the bridge’s hidden medium. The host runs its own MAC and holds its own identity, exactly as it would behind a radio; what it lacks is a radio, and therefore a node of its own standing between it and the medium.

The server presents a ULCP device on such an interface. That device’s radio is the bridge: what the host transmits is relayed by the relay procedure like anything else, and what the relay copies to the interface is delivered to the host as a reception. The device MUST NOT advertise CAP_MAC_BACKHAUL, because there is no node for a backhaul to connect the host to, and a participant MUST NOT attach to one: a bridge’s forwarding policy is its participants’ repeaters, and a host interface has none to offer.

A crossing to or from a host spends one flood hop rather than the two of § A Crossing Is Two Repeater Hops, since no repeater stands between the host and the medium. What makes this safe is unchanged: a host’s frames reach the air only where a participant’s node transmits them, and that node applies the whole forwarding procedure—duplicate suppression, hop accounting, region and signal policy, duty enforcement—to them as to anything else it hears.

Frames delivered to a host MUST carry no signal measurements. The measurement that accompanied such a frame across the tunnel describes a reception on another segment by another radio, and reporting it here would attribute it to a reception that did not happen. This is the same fact the crossing model already records as a trace record whose zero signal bytes say nothing was measured.

The server MUST rate-limit each host interface. A host spends no airtime of its own, which removes the natural bound a radio imposes, while every frame it injects is transmitted by every participant’s node.

The transport carrying ULCP to a host is a local binding and is outside this appendix. Whatever it is, it is a ULCP transport like any other and its attach and detach are the establishment and closure of the underlying connection. A deployment MUST NOT expose that binding beyond a host it trusts as it would trust a device on the end of a cable: reaching it is an RF presence on every segment the bridge touches, and a binding that meets Provisioning Security by physical possession also carries whatever key provisioning and administrative authority the device offers.

LAN Interfaces

A participant MAY join an IP multicast group as an interface: the server beside its radio and its clients, a client beside or instead of its radio. Each datagram from the group is a frame arriving on that interface, carrying no measurements, and each frame sent to the interface is one datagram.

No repeater stands between the group and the bridge. A frame from the group comes either from a bridge, whose node has already forwarded it, or from another node, whose frame no repeater has seen; a crossing from such a node spends one flood hop, as for a host interface. Nothing on the bridge’s side stops a frame circulating between two participants on one LAN, or between a participant and its own multicast echo, so a participant MUST carry a frame across a LAN interface at most once, in either direction, within 30 seconds, judged by the frame’s octets. Every frame this suppresses is one the receiving nodes have already seen.

The server MUST rate-limit its LAN interface, as it does a host interface: anyone on the network can send to the group.

A client with both a radio and a LAN interface copies each frame from its radio, its LAN interface, or the tunnel to the other two.

Relay Procedure

For a frame arriving on interface I, the server:

  1. Discards it if it has grown stale (see Keepalive and Reconnection).
  2. Charges it against I’s traffic limits, and discards it if the limit is spent.
  3. Applies the exit clamp, if one is configured.
  4. Copies it to every interface except I, subject to any configured per-interface-pair rules.

There is no step that reads what the frame means. The server MUST NOT suppress duplicates beyond what LAN interfaces require, account for hops, match source routes, prepend trace entries, or apply signal-quality thresholds; those belong to the participants’ nodes, which apply them to bridged traffic and local traffic alike. A frame the server cannot parse is carried unchanged like any other.

Clients apply nothing at all beyond the framing rules above and the LAN interface rule: a client relays bytes between its radio, its LAN interface, and the tunnel.

Traffic Limits

The server SHOULD rate-limit per client. An authenticated but misbehaving client is the realistic failure mode of a bridge, and the duty ledgers of the devices at the far end should be the backstop, not the policy.

A limit counts everything the client’s node transmits, which includes that node’s repeats of frames the bridge itself handed it. A crossing therefore charges a little against every participant, not only the one whose segment the traffic came from. Budgets should be set with that in mind.

Exit Clamp

A deployment MAY configure the server to clamp the remaining flood budget of frames passing through it. This is the one thing the server does that depends on a frame’s contents, and it exists so that an operator can pull a bridge’s reach in quickly without reconfiguring every device behind it.

When a clamp of n is configured, the server rewrites FHOPS_REM to n for any frame whose FHOPS_REM exceeds it. The flood hop count is dynamic routing metadata excluded from the MIC, so the packet remains authentic—this is the same field a repeater decrements. The server MUST NOT alter FHOPS_ACC, which is a record of hops already taken; MUST NOT add the field to a frame that carries none, since a sender that omitted it meant the frame not to be flooded onward; and MUST NOT discard a frame it cannot parse, which is carried unchanged.

The clamp is off by default. A crossing already spends two flood hops, which bounds a bridged flood without any configuration, and a clamp narrows the reach of every deployment behind the bridge at once. A clamp of 0 stops bridged floods at the egress segment while leaving traffic addressed to the participants’ own nodes unaffected.

Acknowledgements Across a Bridge

Acknowledgements cross as ordinary traffic. A destination’s MAC ack is transmitted by its node, reaches that node’s attached host, crosses the tunnel, and is forwarded back toward the originator by the repeaters along the way—two hops for the crossing, as for anything else.

The round trip therefore costs four flood hops of the sender’s budget: two out and two back. A sender whose budget does not cover the round trip receives no acknowledgement even though the packet arrived. Where an exit clamp is configured, the returning ack is clamped too, and route failure recovery cannot repair that: the restored flood is clamped the same way.

Source-routed hops spend nothing from FHOPS, so explicit routes cross bridges at any depth. Ack-requesting traffic that crosses a bridge SHOULD either carry a Trace option—the participants’ nodes record themselves in it, which is what makes the trace routable—or be sent along a known source route.

Operational Guidance

  • Budget for two hops. Traffic expected to cross a bridge needs a flood budget that covers the crossing at both ends, and twice that if an acknowledgement is expected back.
  • Co-located clients. Two clients whose radios share a segment cause every copied frame to be handed to that segment twice. Their nodes’ duplicate caches keep it from being transmitted twice, but the tunnel traffic is real; per-interface-pair rules are the place to exclude one from the other’s fan-out.
  • Region and signal policy. These are configured on each participant’s device, where the decision to transmit is actually made. A bridge whose segments sit in different regions relies on each node’s own region matching.
  • Leaf participants. A participant whose device does not repeat still reaches the whole bridge and is reachable from it. This is the right configuration for a device that should benefit from a bridge without spending its segment’s airtime on other people’s traffic.

Security Considerations

TLS provides tunnel authentication, integrity, and replay protection; the shared secret or pinned credential is the sole admission control. UMSH frames are already end-to-end authenticated and encrypted at the MAC layer, so the tunnel’s confidentiality mainly shields routing metadata—hints, options, traffic volume—from path observers.

Using a node identity as a tunnel credential does not let the two protocols’ signatures be confused for one another: the TLS 1.3 CertificateVerify payload is domain-separated by a fixed 64-octet padding prefix and context string that no UMSH signed structure begins with.

A compromised client credential is equivalent to granting the attacker an RF presence on every segment the bridge touches: it can inject arbitrary well-formed frames and replay captured ones. It cannot forge other nodes’ authenticated traffic. The damage is bounded by the same mechanisms that bound a hostile local transmitter, and by the same parties: every frame it injects is subject to the duplicate suppression, hop accounting, region policy, and duty-cycle enforcement of the node that would have to transmit it. Per-client rate limits, an exit clamp, and revocation of the client’s credential bound it further.

A backhauled host is off the medium and is delivered only what its own node transmits, so promiscuous delivery grants it nothing beyond the repeats it exists to carry. Operators should still treat the server as privileged infrastructure: it observes the metadata of every frame every participant’s node transmits.

Security Considerations

This chapter consolidates the security properties, limitations, and implementation guidance that are distributed throughout the specification. It is intended as a reference for implementers and reviewers evaluating UMSH’s security posture.

Threat Model

UMSH is designed for a shared radio medium where any device in range can observe and inject packets. The threat model assumes:

  • Passive eavesdroppers can observe all traffic on the channel, including packet timing, size, hint values, and frame counters.
  • Active attackers can inject, replay, modify, or selectively drop packets.
  • Compromised nodes may leak their long-term private keys, channel keys, or both.

UMSH does not assume a trusted infrastructure, a reliable transport, or a synchronized clock.

What UMSH Protects Against

Eavesdropping. When encryption is enabled, payload content is protected by AES-256-CTR keyed with material derived from ECDH (unicast) or the channel key (multicast). An observer without the relevant key cannot recover plaintext.

Forgery. All authenticated packets carry a MIC computed with S2V (AES-CMAC). An attacker who does not possess the encryption and authentication keys cannot produce a valid MIC. The MIC size determines the forgery resistance—from 2^-32 (4-byte MIC) to 2^-128 (16-byte MIC).

Sender impersonation on signed frames. A frame signature covers the header, frame counter, and payload, and on a channel the channel itself. Only the holder of the signing key can produce one, so a verified signature attributes a broadcast or multicast to its sender, which a channel MIC cannot.

Replay attacks. Monotonically increasing frame counters allow receivers to detect and reject replayed packets. The backward window and MIC cache provide tolerance for out-of-order delivery without weakening replay protection.

Nonce misuse. The AES-SIV construction derives the CTR IV from the key, associated data, and plaintext, so accidental nonce reuse (e.g., due to a buggy counter implementation) does not produce the catastrophic plaintext leakage that would occur with AES-GCM or raw AES-CTR. In the worst case, an attacker can detect when two packets carry identical associated data and plaintext—the keys and other traffic remain uncompromised.

Long-term key compromise (with PFS). If a PFS session was active and the ephemeral keys were properly erased, traffic from that session cannot be retroactively decrypted even if the long-term private keys are later compromised.

What UMSH Does Not Protect Against

Traffic analysis. A passive observer can see packet timing, frequency, size, hint values, frame counters, and flood hop counts—all in the clear. This reveals communication patterns (who is active, how often, rough network topology) even when payloads are encrypted. Hint values are stable for a given identity, enabling long-term tracking of a node’s activity.

Multicast sender impersonation. Multicast authentication is based on the shared channel key. Any node possessing the key can construct a valid packet with any claimed source address, and other channel members cannot cryptographically distinguish the true sender from an impersonator. A multicast with a verified frame signature is the exception, but an unsigned multicast, or one whose signature does not verify, carries no such attribution. See Multicast Sender Authentication.

Selective packet dropping. A compromised or malicious repeater can selectively drop packets without detection. UMSH provides no mechanism to verify that a repeater faithfully forwarded a packet. The flood routing model provides redundancy (multiple paths), but a strategically positioned adversary can still disrupt delivery.

Denial of service. An attacker can flood the radio channel with valid-looking packets, forcing receivers to expend computation on cryptographic verification. The 3-byte destination hint reduces this cost (only ~1 in 16,777,216 unicast packets will trigger verification for any given node), but the shared medium provides no isolation. The EMERGENCY channel’s priority forwarding could be abused to amplify DoS traffic, though the frame-signature requirement limits this to attackers who possess a valid Ed25519 keypair.

Bounded extra forwarding via route recovery. The Route Retry option intentionally allows one extra forwarding wave for an already-seen authenticated packet when the sender is recovering from a stale route. Because the option is dynamic and not MIC-protected, an observer who can copy a packet can also add the option and potentially trigger that extra forwarding attempt. This is a real amplification tradeoff, but it is tightly bounded: the cache key distinguishes only the original packet and the route-retry form, not an unbounded sequence of retries. Implementations should preserve that bound and must not treat arbitrary dynamic-option changes as creating new forwarding identities.

Peer-registry exhaustion by first-contact senders. If an implementation automatically learns peers from inbound full-key packets, an attacker can generate many distinct keypairs and send valid first-contact traffic in an attempt to fill the peer table. If successful, this can crowd out legitimate peers or prevent future first contact. Implementations should distinguish between explicitly configured peers and opportunistically learned peers. Explicit peers should be pinned and must not be displaced by auto-learning. Opportunistically learned peers should be bounded separately or recycled with an eviction policy such as least-recently-seen replacement. Multicast traffic should not require persistent peer registration merely to deliver application traffic in large group-chat scenarios; where sender identity is not already known, implementations may need to fall back to best-effort duplicate suppression rather than strict per-peer replay tracking.

Traffic amplification via broadcast or multicast requests. An unsigned broadcast packet is unauthenticated, a multicast packet may be attributable only to a shared channel key, and even a signed one may come from an ephemeral source identity. An attacker can exploit this by sending a request that appears to warrant a response or some other follow-on action from every receiving node. If the request does not include a complete trace, recipients do not learn a specific return path. Any per-node reply may therefore fall back to flood routing, using the inbound FHOPS_ACC as a distance estimate or flooding more broadly if no better routing state exists. The result is an amplification attack: one injected request can trigger many independent flood-routed responses, consuming airtime and effectively causing a distributed denial of service. Implementations and application protocols must therefore treat broadcast and multicast requests as fan-out hazards. They should not automatically generate per-node responses unless those responses are explicitly designed to avoid amplification through mechanisms such as route learning, strict rate limits, randomized suppression, aggregation, or making the request one-way only.

Non-repudiation. UMSH’s MIC is computed with symmetric pairwise keys that both sender and recipient possess, so a recipient cannot cryptographically prove to a third party who authored a given packet—either party could have constructed it. However, UMSH does not claim to provide deniability. Real-world deniability depends on the entire system: usage patterns, device forensics, metadata, and interactions with other systems. The symmetric MIC is a narrow property, not a deniability guarantee. A frame signature (as required on the EMERGENCY channel) or an EdDSA signature in an identity payload gives up even this narrow property—a signature can only be produced by the private key holder, and anyone holding the signed bytes can check it.

Forward secrecy without PFS sessions. Normal unicast traffic uses stable pairwise keys derived from long-term ECDH. If a node’s long-term private key is compromised, all past and future unicast traffic with that node can be decrypted. Forward secrecy requires explicit use of PFS sessions.

Anonymous channel membership. Possessing a channel key is both necessary and sufficient for channel membership. There is no mechanism to verify who holds a key, revoke access to a specific node without re-keying the entire channel (except via managed channels), or detect how many members a channel has.

Implementation Requirements

The following requirements are critical for security. Failure to implement any of them correctly can compromise the properties described above.

Frame Counter Monotonicity

The frame counter must strictly increase for each packet sent in a given traffic direction. Reusing a counter value with the same key undermines replay protection and, in the worst case, can leak information about plaintext differences (though the AES-SIV construction limits the damage). See Frame Counters.

Frame Counter Persistence

A node must not reuse frame counter values across reboots. Implementations must either persist the counter to non-volatile storage or advance it by a large margin on startup. A node must likewise persist each known peer’s receive baseline and restore it at startup; one that loses a peer’s baseline accepts a replay of that peer’s earlier frames as first contact. If writing to non-volatile storage, care must be taken to avoid wearing out the storage medium. See Counter Persistence and Counter Resynchronization.

Ephemeral Key Erasure

PFS sessions derive their security from the guarantee that ephemeral private keys are erased when the session ends. Implementations must ensure that ephemeral keys are:

  • Never written to persistent storage, swap files, or logs
  • Explicitly zeroed in memory upon session termination (not just freed—freed memory may not be overwritten promptly)
  • Not retained in core dumps or crash reports

Failure to erase ephemeral keys eliminates the forward secrecy property entirely. See Session Lifetime.

Constant-Time MIC Verification

MIC comparison must use constant-time comparison (e.g., a fixed-iteration XOR-and-OR loop) rather than memcmp or similar short-circuiting functions. A timing side channel in MIC verification allows an attacker to incrementally guess MIC bytes by measuring response time.

Public Key Validation

Implementations must reject malformed Ed25519 public keys before converting them to X25519 form. Accepting a malformed key can produce a low-order X25519 point, resulting in a shared secret of zero—which would cause all pairwise keys to be identical across different peers. See Ed25519 to X25519 Conversion.

Reserved Bits

Packets with non-zero reserved bits in the Security Control Field must be dropped. Accepting unknown bit patterns could indicate a protocol version mismatch or a malformed packet; processing them risks undefined behavior.

Metadata Exposure

Even with encryption enabled, the following information is visible to a passive observer. Which fields are present depends on the packet type:

FieldPacket typesWhat it reveals
Packet timing and frequencyAllCommunication patterns—when a node is active, how often it transmits
Destination hint (3 bytes)UnicastStable per-identity; enables tracking a node’s correspondents over time
Source hint (3 bytes)Unicast, unencrypted multicast, broadcastStable per-identity; enables tracking a node’s activity over time
Channel identifier (2 bytes)Multicast, blind unicastStable per-channel; reveals which channel a packet belongs to
Frame counterAll authenticatedMonotonically increasing; reveals total packet count and transmission rate
Flood hop countAll with FHOPSReveals approximate distance from the original sender
Packet sizeAllMay correlate with payload type or content length
MICAll authenticatedUnique per-packet; usable as a packet fingerprint for correlation across hops
Ack MIC (4 bytes)MAC AckPrefix of the acknowledged packet’s MIC; links the ack to the original packet and confirms its delivery. Carries no explicit endpoint identifier (the MAC Ack has no destination hint), but the link to the original packet remains a correlation vector

In encrypted multicast, the source address is encrypted inside the ciphertext. In blind unicast, both the source and destination addresses are encrypted using the channel key—only the channel identifier remains in the clear. Normal unicast exposes both the destination hint and source hint (or full source key) to passive observers.

Frame Counter Correlation and PFS

If a device uses a single monotonic frame counter across all traffic (including PFS sessions), an observer can correlate PFS session traffic with the device’s long-term identity by observing counter continuity. Implementations concerned with PFS unlinkability should consider using independent frame counters for PFS sessions. See Wire-Level Privacy.

Hint Stability and Tracking

Because hints are derived deterministically from public keys, they remain stable for the lifetime of a node identity. An observer who associates a hint with a physical location or person can track that identity across sessions, power cycles, and network changes. The only countermeasure is generating a new identity (a new Ed25519 keypair), which requires all peers to re-learn the new public key.

Cryptographic Design Rationale

AES-SIV over AES-GCM

UMSH uses AES-SIV (RFC 5297) rather than AES-GCM. AES-GCM is catastrophically vulnerable to nonce reuse: a single repeated nonce leaks the authentication key and allows forgery of arbitrary messages. On a mesh network where counter management is distributed across many independent nodes and persistence across reboots is not guaranteed, nonce reuse is a realistic failure mode. AES-SIV degrades gracefully—nonce reuse reveals only whether two packets are identical, without compromising keys or enabling forgery. Deterministic SIV-style alternatives built on other primitives, such as ChaCha20-Poly1305-SIV, were considered, but none is as mature or as widely analyzed as RFC 5297. See the FAQ.

AES-256 over AES-128

UMSH’s asymmetric layer—Ed25519 identities and X25519 key agreement—provides roughly 128-bit classical security, so AES-128 would be a matched choice against classical adversaries, and a cheaper one on low-power hardware. UMSH uses AES-256 anyway, because the symmetric layer is the one part of the protocol that can meaningfully survive a quantum adversary.

No standardized post-quantum key exchange or signature scheme fits within a LoRa frame budget, so the asymmetric layer cannot be hardened against Shor’s algorithm. Recovering a private key that way, however, requires the complete public key. Most UMSH traffic exposes only a 3-byte hint, and blind unicast conceals even a full first-contact source key inside the channel-encrypted address block. A deployment that avoids exposing full public keys and leans on shared channel keys therefore retains meaningful—though reduced—security against a quantum adversary: physical compromise of any channel member defeats it, but a purely over-the-air attacker is left facing the symmetric layer alone. That fallback is only as strong as the symmetric primitives, and Grover’s algorithm halves a symmetric key’s effective strength: AES-128 would be reduced to roughly 64-bit post-quantum strength, while AES-256 remains far out of reach. Choosing AES-256 preserves the symmetric fallback at the cost of extra cycles and code size on constrained devices.

Stable Keys over Ratcheting

UMSH uses stable pairwise keys rather than a ratcheting protocol. Ratcheting provides forward secrecy per-message but requires synchronized state between sender and receiver. On a lossy, high-latency mesh where packets are routinely dropped, duplicated, or delivered out of order, ratchet state can desynchronize—potentially requiring expensive resynchronization exchanges over a slow radio link. UMSH’s stable keys combined with per-packet counter and salt inputs provide per-packet IV uniqueness without requiring synchronized state. Optional PFS sessions provide forward secrecy when needed, without imposing ratcheting’s fragility on all traffic. See the FAQ.

Single Keypair for Signing and Key Agreement

UMSH uses a single Ed25519 keypair per node for both identity (signing) and key agreement (via X25519 conversion). Standard guidance recommends separate keys, but the alternative would require distributing an additional 32-byte X25519 public key per identity and cryptographically binding it to the Ed25519 key. On a ~255-byte LoRa frame, this overhead is significant. The Ed25519/X25519 conversion is a well-understood, deterministic mapping over birationally equivalent curves, used by Signal’s X3DH and libsodium. See Ed25519 to X25519 Conversion.

Channel-Specific Considerations

Named Channel Security

Named channels derive their key from a human-readable name via HKDF-Extract. Anyone who knows (or guesses) the name can derive the key. Named channels should be treated as public—they provide a shared namespace, not confidentiality. Long, high-entropy names offer practical obscurity but should not be relied upon for security.

Emergency Channel Integrity

The EMERGENCY channel requires unencrypted transmission, full source key (S=1), and a frame signature. These requirements ensure that emergency traffic is universally readable and cryptographically attributable. The channel commitment keeps a signed emergency message from being replayed on another channel that shares its identifier, and signed-frame counters keep it from being replayed on this one, with one limit: a receiver that has no prior knowledge of the sender, or has discarded or lost it, accepts a replay of that sender’s earlier message once. However, an attacker with a valid Ed25519 keypair can still send fraudulent emergency messages—the signature proves only that the sender possesses the key, not that the emergency is real. Social and operational controls (e.g., reputation, identity verification) are needed to complement the cryptographic guarantees.

Blind Unicast Key Binding

Blind unicast payload keys are derived by XORing the pairwise unicast keys with the channel’s multicast keys. This ensures that decrypting a blind unicast payload requires both the pairwise shared secret and the channel key. Compromise of one without the other is insufficient.

Contact Exchange

Code Strength

A device in the middle of a contact exchange must send its commitment before it learns the other side’s nonce, and cannot change its nonce after it learns it. Each attempt therefore gives it one guess at making both holders see the same code: a one-in-a-million chance, paid for with an attempt both holders see fail. The code proves only that the two devices talking are the two in the holders’ hands; it says nothing about the names those devices then send.

Session Key Erasure

Session keys are ephemeral keys and fall under the same erasure requirements as PFS session keys. Because a new key is drawn for every attempt, Nearby rows heard in one attempt cannot be linked by key to the next.

Correlation That Remains

A Hello names a hardware model, and a rare model narrows who is nearby. The long-term identity keeps sending its ordinary traffic while a session is open, so an observer can correlate the two by timing and location.

Amateur Radio Operation

UMSH supports three distinct operating modes for devices or repeaters deployed on spectrum where amateur operation and unlicensed operation may coexist.

Operating Modes

Unlicensed

In Unlicensed mode, the node operates only under non-amateur rules.

  • Locally originated packets are treated as unlicensed traffic.
  • Encryption is enabled by default.
  • A repeater MAY forward any packet it may lawfully retransmit under unlicensed rules.
  • Maximum transmit power and duty cycle are determined by local rules for unlicensed operation.
  • If a forwarded packet carries a station callsign, the repeater MUST remove it rather than replacing it.
  • The repeater SHOULD NOT add its own station callsign.

The specific requirements for unlicensed transmission vary by jurisdiction and frequency, but may include restrictions on transmit power, antenna gain, and/or duty cycle.

Licensed-Only

In Licensed-Only mode, all locally originated and forwarded traffic is treated as amateur-radio traffic.

  • Encryption SHALL NOT be enabled for any packet. All encrypted packets encountered SHOULD be immediately dropped.
  • Locally originated packets, MAC Acks included, MUST include an operator callsign.
  • Restrictions on transmit power and duty cycle are generally more relaxed.
  • A repeater SHALL NOT forward packets that are missing an operator callsign.
  • A repeater SHALL replace or insert the station callsign option with its own callsign on every forwarded packet.

Tip

While blind unicast is not categorically forbidden by this mode, the expected utility of using it without encryption is limited.

Hybrid

In Hybrid mode, the node may operate under either authority depending on the packet.

  • A repeater SHOULD add or replace the station callsign option on forwarded packets.
  • Packets carrying an operator callsign MAY be forwarded under amateur-radio authority.
  • Packets lacking an operator callsign MAY still be forwarded, but only when the retransmission can lawfully occur under unlicensed rules.
  • If the packet has encryption enabled, the transmission MUST be treated as unlicensed traffic, including using power and any other regulatory limits appropriate for unlicensed operation.

Hybrid mode is useful where amateur stations may use higher power for qualifying amateur traffic, while still allowing encrypted or otherwise unlicensed-only traffic to transit the same repeater at unlicensed settings.

Locally Originated Packets

The MAC layer should apply the following transmit rules:

  • Unlicensed: no amateur-specific restriction is implied, but restrictions on tx power and duty cycle may apply.
  • Licensed-Only: encrypted packets must be rejected, and an operator callsign is required. Max transmit power may increase.
  • Hybrid: encrypted packets are allowed, but they must be transmitted under unlicensed constraints rather than amateur-only ones.

Limitations & Open Items

Known Limitations

No MAC-Layer Fragmentation

UMSH intentionally does not define a fragmentation mechanism at the MAC layer. The MAC layer delegates fragmentation to higher-layer protocols carried in the payload (see Layer Separation). LoRa payloads are typically limited to approximately 200–250 bytes, and UMSH header overhead (FCF, addresses, SECINFO, MIC) consumes a significant portion of this budget. Higher-layer protocols that require payloads larger than a single frame must provide their own segmentation—for example, CoAP block-wise transfer or 6LoWPAN fragmentation.

Multicast Sender Authentication

Multicast channels use a shared symmetric key. Any node possessing the channel key can send packets with any claimed source address, and other channel members cannot cryptographically verify from the MIC that the claimed sender actually produced the packet. The MIC authenticates only channel membership, not individual sender identity. This is a fundamental property of symmetric-key multicast and is shared by other protocols with similar designs, including MeshCore.

A sender can make a multicast attributable by signing it (the SCF’s D bit): a verified frame signature proves which key sent the packet. The signature costs 64 bytes, and a receiver can check it only against a key it holds or that the packet carries in full (S set), so most multicast traffic is unsigned and stays attributable only to the channel.

Known Hint Collision Properties

MeshCore originally used 1-byte hints for source, destination, and source-routing addresses, placing the birthday bound at just 16 nodes—far too low for practical networks. UMSH uses 3-byte hints for node addresses and 2-byte hints for router addresses in source routes and traces.

As a concrete example, consider a regional network of approximately 600 active nodes (roughly the scale of the Oregon MeshCore network, concentrated in the Portland area). The probability of at least one collision among all nodes for a given hint size:

Hint sizeCollision probability (600 nodes)
1 byte (256 values)~100%
2 bytes (65,536 values)~94%
3 bytes (16,777,216 values)~1%

Node hints (3 bytes)

The destination hint is a prefilter: a match causes the receiver to attempt full cryptographic verification. A false positive wastes computation on every packet exchanged between the two colliding nodes, for the lifetime of both identities. The cost is persistent and proportional to traffic volume. The source hint is used for source identification, traffic attribution, and diagnostics.

With 3-byte hints the collision probability drops to ~1% even in a 600-node regional network. The only remedy for a collision is for one node to generate a new identity (a new Ed25519 keypair), which for a chat node means all peers must re-learn the new public key. The 3-byte size makes this scenario rare.

Router hints (2 bytes)

Router hints are used in source-route and trace options. A router hint collision causes an unintended repeater to forward the packet; MIC-based duplicate suppression ensures each repeater forwards a given packet at most once, so collisions add traffic but not loops or incorrect delivery.

Source routing is an inherently local operation—only repeaters within radio range of the transmitting node can act on the hint. For a local population of ~50 repeaters:

  • 1-byte hints: ~46% collision probability
  • 2-byte hints: ~1.9% collision probability

2-byte router hints reduce the collision probability by roughly 24× relative to 1-byte hints for typical deployments, at a cost of 1 extra byte per hop in source-route and trace options.

Open Issues

Intermediate Node Error Feedback

Problem

When a packet cannot be delivered—for example, because a bridge’s backhaul link is down, or because a source-routed path is broken—intermediate nodes have no reliable way to inform the original sender. The sender can only detect failure by waiting for a MAC ack that never arrives.

This timeout-based detection is slow and provides no diagnostic information: the sender cannot distinguish a slow destination from a broken path.

Two independent gaps make this hard to address:

  • Routing: an intermediate node needs a return path to send anything back. The sender’s 3-byte SRC hint provides a destination address, but that alone is not enough—the error packet needs to know how to get there. Flood routing is not an option: flooding an error response across the mesh in response to a delivery failure would be prohibitively expensive. A return path is only available if the original packet carried a complete trace, whose accumulated hops already describe the return path from the receiver back toward the original sender.

  • Authentication: an intermediate node cannot send an authenticated reply without the sender’s full public key. Any error reply sent without it is unencrypted and unauthenticated. Only the final destination—which has the full source key and performs ECDH—can send a fully authenticated reply.

Without a complete trace, there is no viable return path and no error feedback is possible.

Possible Approach: Trace Return Path

If the original packet carries a complete trace, an intermediate node can use the accumulated hops directly as a source route back toward the original sender and emit an error packet along that path, addressed to the sender’s 3-byte SRC hint.

This is opt-in by the sender: include a trace to signal willingness to receive error feedback; omit it to suppress errors. No special flag or option is needed. If no complete trace is present, the intermediate node has no viable return path and should remain silent.

The on-wire format for such an error packet would likely be similar or identical to the Hop Signal mechanism described above—a compact notification carrying a signal type and a MIC reference—differing only in that it carries a source route and travels end-to-end rather than remaining local. Defining a single format that covers both cases would reduce protocol surface area.

Any such error is unencrypted and unauthenticated. Senders MUST treat it as untrusted diagnostic information only—a forged error is equivalent in effect to a dropped packet, which is already in the threat model.

This remains an open design question. No mechanism is specified in the current version of this protocol. The format and semantics of error reports—error codes, triggering conditions, encoding—have not yet been defined.

FAQ

Can an attacker spoof a MAC Ack to make the sender believe a packet was delivered?

Not without the pairwise key. A MAC Ack carries two fields: a public ack MIC (the first 4 bytes of the original packet’s on-wire MIC, used only for correlation) and a 4-byte ack tag, the MIC of the ack itself, bound to the original packet’s full 16-byte S2V output. Only the ack tag authenticates the ack, and computing it requires the MIC key (pairwise for unicast, or the combined blind unicast key for blind unicast). A passive observer who intercepts the original packet can read the on-wire MIC and thus reproduce the public ack MIC, but cannot produce the ack tag without K_mic. Blind forgery of the 4-byte tag succeeds with probability 2^-32 per attempt, only while the original packet awaits acknowledgement, and with no offline check; and even a successful forgery only causes a false delivery confirmation for one exchange (a reliability denial-of-service), never a confidentiality break.

Doesn’t blind unicast have a circular dependency between the MIC and address decryption?

No. The MIC field is located at the end of the packet and can be read directly from the wire. It is computed using the blind unicast payload keys, which combine the pairwise shared secret with the channel key. The receiver reads the MIC, uses it (together with the channel’s K_enc_channel) as the IV to decrypt ENC_DST_SRC, and then derives the pairwise keys from the recovered source address. The pairwise keys are XORed with the channel keys to produce the blind unicast payload keys, which are used to authenticate and decrypt ENC_PAYLOAD. If either address has been tampered with, the derived pairwise keys will be wrong and payload authentication will fail. There is no circular dependency—only a specific processing order (see Blind Unicast).

Can source-routed packets loop if router hints collide?

No, for two reasons. First, the forwarding path is bounded by the number of router hints in the source route plus the flood hop count—a packet cannot be forwarded more times than the sum of these values. Second, duplicate suppression (see Duplicate Suppression) ensures that each repeater forwards a given packet at most once (identified by MIC). Even if a router hint collision causes an unintended repeater to forward the packet, the probability of subsequent hints also colliding with nearby repeaters drops dramatically at each hop, making extended misrouting extremely unlikely.

What happens when a cached source route goes stale?

If an ack-requested packet sent on a cached source route exhausts its retry budget, the sender should treat that route as failed and return temporarily to route-discovery mode. The recommended recovery is:

  • discard or demote the stale route
  • re-attempt the same logical packet
  • remove the stale source route
  • add or refresh flood hops
  • include a Trace option so a fresh path can be learned
  • set the Route Retry option

The key point is that this is still the same logical packet, not a new application message. The destination therefore accepts it at most once according to the normal replay rules, while repeaters treat the Route Retry form as a distinct forwarding opportunity for duplicate-suppression purposes.

Why doesn’t UMSH define a dedicated path-discovery packet type?

The existing primitives are sufficient. A node can discover a path by sending a flooded packet (broadcast, unicast, or beacon) with the Trace option present. Repeaters prepend their records as they forward. The recipient can use the router hints of a complete trace directly as a candidate source route. This avoids adding protocol complexity for a function that composes naturally from existing features. See Path Discovery for the full procedure.

How does UMSH handle frame counter overflow?

The 4-byte frame counter wraps naturally at 2^32. Replay detection uses modular arithmetic: delta = (received - last_accepted) mod 2^32. A positive delta within a reasonable forward window is accepted; zero or excessively large deltas are rejected. This means overflow is not a special case—it is handled identically to any other counter increment. See Replay Detection.

Can a multicast channel member impersonate another member?

Yes, unless the packet is signed. Multicast authentication is based on the shared channel key, not on individual sender identity. Any node with the channel key can construct a valid packet with any claimed source address. This is an inherent property of symmetric-key multicast and is shared by other protocols with similar designs. A multicast with a verified frame signature is the exception: only the holder of the signing key could have sent it. The EMERGENCY channel requires one. See Multicast Sender Authentication.

This does not apply to blind unicast. Blind unicast payloads are authenticated using combined keys that require both the pairwise shared secret and the channel key, so only the true sender can produce a valid payload and only the intended recipient can read it.

When should the S flag (full source address) be set?

The S flag controls whether the full 32-byte source public key or a compact source hint is included in the packet. Set S when:

  • This is a first-contact transmission and the receiver has never seen the sender’s public key before.
  • The sender wants to allow any receiver to perform ECDH and authenticate the packet without prior state.
  • The sender is using an ephemeral keypair (anonymous request pattern).

Leave S clear when the receiver is known to have the sender’s full public key cached—for example, after a prior advertisement, identity exchange, or any earlier S=1 packet. Using the compact hint saves 29 bytes per packet in unicast (3-byte hint vs 32-byte key), which is significant on LoRa.

Receivers that see an unknown source hint on an authenticated packet should treat it as an authentication failure (the cached key lookup fails, so decryption or MIC verification will fail). The sender can retransmit with S=1 to provide the full key.

How does a MAC Ack get routed back to the original sender?

MAC acks are end-to-end: the final destination generates the ack, not any intermediate repeater. The ack is routed back to the original sender using whatever routing state the destination has learned—typically a source route derived from the inbound packet’s trace, or a flood scoped by the inbound packet’s FHOPS_ACC together with any learned region-code options. This is the same route learning mechanism used for all communication, not an ack-specific feature.

Repeaters do not generate acks themselves. Instead, a repeater can confirm successful forwarding by overhearing the next hop’s retransmission of the same packet (see Forwarding Confirmation).

Why does UMSH use stable pairwise keys instead of a ratcheting scheme like the Signal Protocol?

LoRa mesh networks have high latency, low bandwidth, and unreliable delivery—properties that are hostile to ratcheting protocols. Ratcheting requires reliable in-order message delivery to keep both sides synchronized; a single lost message can desynchronize the ratchet and require an expensive recovery handshake. In a mesh where packets may be lost, duplicated, or arrive out of order, this would lead to frequent resynchronization storms. Stable pairwise keys derived from a single ECDH are simple, stateless, and robust to packet loss. The frame counter and optional salt still provide per-packet uniqueness, while the AES-SIV construction provides an additional safety margin against nonce misuse.

When forward secrecy is needed, UMSH provides PFS sessions—a two-message handshake where both nodes exchange ephemeral node addresses and communicate using session-specific keys for an agreed duration. PFS sessions add no per-packet overhead once established, and the private keys for the ephemeral addresses are erased when the session ends. This provides perfect forward secrecy without the fragility of continuous ratcheting.

What happens if a 2-byte channel identifier collides across different channel keys?

The 2-byte channel identifier is a hint, not a unique identifier. If two different channel keys happen to produce the same 2-byte channel ID, a receiver configured with both keys will attempt to process the packet with each candidate key. Only the correct key will produce a valid MIC, so the wrong candidate will be rejected during authentication. The cost is wasted computation, not incorrect behavior. With 2 bytes the collision probability is 1 in 65536, which is higher than a 4-byte hint but still negligible for deployments with a small number of configured channels.

Why use AES-SIV instead of AES-GCM?

AES-GCM is catastrophically vulnerable to nonce reuse—repeating a nonce with the same key completely breaks both confidentiality and authenticity. In a mesh network, nonce management is difficult: nodes may reboot and lose counter state, clocks may not be synchronized, and packets may be retransmitted. AES-SIV (RFC 5297) computes its CTR IV from the key, associated data, and plaintext, so there is no independent caller-supplied nonce to misuse: repeating all inputs produces an identical packet and reveals only the repetition itself. That is exactly the failure mode UMSH wants under counter loss—graceful, not catastrophic. See Encrypted Packets.

How does “deliver to a region, then flood” work?

A sender can include both a source-route option and a flood hop count in the same packet. The source-route directs the packet through specific repeaters, and as each repeater forwards, it removes its own hint. Once all source-route hints are consumed, the packet transitions to flood-based forwarding bounded by FHOPS_REM. This allows targeted delivery to a specific area of the mesh followed by a local flood—useful when searching for a node in a known geographic region without flooding the entire network. See Routing Implications.

Can UMSH support anonymous requests, similar to MeshCore’s ANON_REQ mechanism?

Yes. A node can generate an ephemeral Ed25519 keypair, set the S flag, and use the ephemeral public key as the source address for a single request, then discard the private key immediately afterward. The recipient performs ECDH with the ephemeral public key as normal, encrypts a response to it, and sends it back. The requester’s long-term identity is never revealed. This pattern also provides forward secrecy for the exchange: once the ephemeral private key is discarded, the session cannot be decrypted even if the requester’s long-term key is later compromised. No dedicated packet type is required.

Does UMSH support automatic route learning?

Yes. A node that wants to learn a source route to a peer sends any flooded packet (unicast, broadcast, or beacon) with the Trace option present. Repeaters prepend their records as they forward. When the peer receives the packet, a complete trace contains the accumulated path, ordered nearest-repeater-first, and can be used directly as a source-route option on reply packets. Both sides can learn routes simultaneously by including the trace-route option on their outbound packets and caching the results. See Path Discovery for the full path-discovery procedure.

Comparison with MeshCore

This section compares UMSH with MeshCore, a LoRa mesh protocol with similar goals. The comparison is based on MeshCore firmware v1.17.0 and its primary source code and documentation.

Note

This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!

Identity and Addressing

Both protocols use Ed25519 public keys as node identities and perform X25519 ECDH for pairwise key agreement.

AspectUMSHMeshCore
Identity key32-byte Ed25519 public key32-byte Ed25519 public key
Source address in packets3-byte hint (S=0), or full 32-byte key (S=1)1-byte hash (first byte of public key)
Destination address3-byte hint1-byte hash
Channel identifier2-byte derived hint1-byte hash of SHA-256 of channel key

UMSH uses 3-byte hints for node addresses and 2-byte hints for router addresses in source routes and traces, giving 1-in-16,777,216 collision resistance on node identifiers. An explicit S flag includes the full 32-byte source key when needed (first contact, ephemeral keys). MeshCore uses 1-byte source and destination hashes in regular encrypted unicast payloads, while routing paths support configurable 1-, 2-, or 3-byte node hashes. A dedicated ANON_REQ packet type carries the full 32-byte sender public key for first-contact or anonymous exchanges. The tradeoff is that MeshCore saves bytes per address field in the common case, but requires a special packet type for any situation where the full key must be transmitted.

Packet Structure

AspectUMSHMeshCore
Header1-byte FCF with version, type, flags1-byte header with version, type, route mode
Packet types8 (via 3-bit field in FCF)16 payload-type code points (13 assigned; via 4-bit field)
Routing infoCoAP-style options (source route, trace, region, RSSI/SNR thresholds)Path field (up to 64 bytes; 1–3-byte hashes), transport codes
Flood hop countSplit 4-bit FHOPS field (max 15)Implicit via path length
Region supportOptional region code optionTransport codes (2 × 16-bit)

UMSH separates routing metadata into composable options, allowing packets to carry source routes, traces, signal-quality thresholds, and region codes independently. MeshCore uses a simpler flat structure with a path field and route-type bits.

Cryptography

AspectUMSHMeshCore
Encryption algorithmAES-256-CTR (AES-SIV, RFC 5297)AES-128-ECB
AuthenticationS2V (AES-CMAC), 4/8/12/16-byte MICHMAC-SHA256 (truncated to 2-byte MAC)
Key derivationHKDF-SHA256 with domain-separated keys (K_enc, K_mic)Raw ECDH shared secret used directly
Key separationSeparate 32-byte encryption and 32-byte MIC keysSame shared secret for both AES key (first 16 bytes) and HMAC key (full 32 bytes)
Nonce misuse resistanceYes (SIV construction)N/A (ECB mode is deterministic)
Replay protection4-byte monotonic frame counter (timestamp-free)Hash-based duplicate cache (160 entries); timestamps at application layer

The cryptographic gap is substantial:

  • AES-128-ECB is a textbook-insecure mode: it uses no IV or nonce, and identical plaintext blocks produce identical ciphertext blocks, leaking structural information about the payload. AES-CTR with a synthetic IV (as used by UMSH) does not have this weakness.

  • 2-byte MAC truncation in MeshCore means an attacker has a 1-in-65536 chance of forging a valid MAC per attempt, which is marginal for a protocol where an attacker can observe and replay packets at will. UMSH’s 16-byte MIC provides a forgery probability of 2^-128.

  • No key separation in MeshCore means the same bytes of the ECDH shared secret serve as both the AES key and the beginning of the HMAC key. UMSH derives independent keys via HKDF with domain-specific labels, which is the standard practice for preventing cross-protocol or cross-purpose key reuse.

  • MAC verification timing: MeshCore’s MACThenDecrypt function uses memcmp to compare HMAC values, which is not constant-time and introduces a timing side channel. This is primarily a concern in contexts where an attacker can measure verification timing with sufficient precision.

Routing

AspectUMSHMeshCore
Flood routingYes, bounded by flood hop countYes (ROUTE_TYPE_FLOOD)
Direct/source routingYes, via source-route optionYes (ROUTE_TYPE_DIRECT)
Hybrid routingSource route + flood hop count in same packetNot supported
Path discoveryTrace option on any flooded packetDedicated PATH payload type
Route learningTrace option accumulates router hints and per-hop signal during flooding; used as a source route by the recipientExplicit returned-path messages
Forwarding confirmationYes (retries with backoff)Not defined
Channel accessCAD with random backoff; SNR-based contention windowsListen-before-talk with random backoff; SNR-based flood retransmit delay
Signal-quality filteringMin RSSI and min SNR optionsSNR-based retransmit prioritization (implicit, no explicit thresholds)
Region-scoped floodingRegion code optionTransport codes

UMSH’s hybrid routing model allows a single packet to be source-routed to a specific area and then flood locally, which is useful for reaching a node in a known geographic region without flooding the entire mesh. MeshCore treats flood and direct routing as mutually exclusive modes selected by route-type bits.

Region-Scoped Flooding

Although both protocols carry a 16-bit value to scope flood forwarding, those values are derived and evaluated very differently.

An UMSH region code is a stable two-byte identifier for a region. It is derived from the configured region name according to the Region Code Encoding, independently of any particular packet. A sender places one or more region-code options in the routing metadata, and a repeater decides whether to forward by comparing those values directly with its configured region codes. The payload remains opaque: changing the payload does not change the region code or require the repeater to perform any payload-dependent calculation.

A MeshCore transport code is packet-specific rather than a stable identifier for a region. For a public region, MeshCore first derives a 16-byte transport key from the region name: the first 16 bytes of SHA-256 over the #-prefixed name. Despite its name, this is not an encryption key; anyone who knows the public region name can derive it. For each packet, the transport code is the first 16 bits of:

HMAC-SHA256(transport_key, payload_type || payload)

MeshCore remaps the reserved results 0x0000 and 0xFFFF to 0x0001 and 0xFFFE, respectively. The routing path and transport-code fields are not included in the HMAC input, so repeaters can extend the path without changing the code. Changing either the payload type or payload bytes does change the code.

When a MeshCore repeater receives a transport-scoped packet, it iterates through the region entries allowed for that routing mode, obtains up to four candidate transport keys for each entry, recomputes the HMAC for each candidate, and compares the result with the packet’s first transport code. It forwards only after finding a match. Thus, MeshCore does not decrypt the payload to make this decision, but its forwarding layer does cryptographically process the payload once per candidate key. The work grows with both the number of candidate keys and the payload length. UMSH instead performs fixed two-byte comparisons against routing metadata.

This payload-dependent transport-code mechanism was introduced in MeshCore commit 03fc9490.

Both protocols support automatic route learning, but through different mechanisms. UMSH uses a Trace option that accumulates a record of each hop as a packet floods—its router hint and the signal it was received with; the recipient caches the accumulated trace as a source route for all subsequent communication with the sender—replies, acknowledgments, and new messages alike (see Route Learning). MeshCore uses a dedicated returned-path message type.

Both protocols define channel access mechanisms. MeshCore checks for preamble or signal detection before transmitting and defers with a randomized backoff (120–360 ms) if the channel is busy, with a forced-transmit safety valve after 4 seconds. Flood retransmissions use a random delay proportional to airtime and a score-based priority derived from received SNR. UMSH uses CAD with random backoff and SNR-based contention windows for collision avoidance. UMSH additionally defines hop-by-hop forwarding confirmation with retries, providing reliability across the forwarding chain that MeshCore does not offer.

Privacy and Blind Modes

AspectUMSHMeshCore
Multicast source concealmentYes (source encrypted inside ciphertext when encryption enabled)Yes (no cleartext source; claimed sender name is encrypted)
Blind unicastYes (source encrypted with channel key, payload with pairwise key)No
Anonymous requestsEphemeral Ed25519 key with S=1 flagDedicated ANON_REQ packet type
Metadata concealmentChannel-key-based, hides sender and/or destination from non-membersGroup sender and content concealed; no blind-unicast equivalent

Both protocols conceal the claimed sender of encrypted group traffic from observers who do not possess the channel key. UMSH encrypts the source address inside the multicast ciphertext. MeshCore group packets have no cleartext source field and place the self-asserted sender name inside the ciphertext. MeshCore does not cryptographically authenticate that group sender name and does not define an equivalent of UMSH blind unicast, which conceals both unicast endpoints from non-members.

Both protocols support anonymous first-contact requests, but through different mechanisms. UMSH uses an ephemeral keypair as the source address with the S flag set—no dedicated packet type is needed. MeshCore defines a specific ANON_REQ payload type that carries the full 32-byte sender public key.

Multicast

AspectUMSHMeshCore
Channel key size32 bytesVariable (shared secret)
Channel identifier2-byte derived hint1-byte hash of SHA-256 of key
Group message authChannel-key-based S2V MICChannel-key-based HMAC (2-byte MAC)
Sender authenticationChannel MIC alone does not verify the sender; a signed multicast is attributable to its sender’s keyNot cryptographically verified (symmetric key limitation)
Source privacySource encrypted when encryption enabledNo cleartext source; claimed sender name encrypted

Both protocols share the fundamental limitation that symmetric-key multicast cannot authenticate individual senders—any channel member can forge a packet with any claimed UMSH source address or MeshCore sender name. UMSH lets a sender opt out of that limitation per packet: a multicast carrying a frame signature (the SCF’s D bit) is attributable to its sender’s key, at a cost of 64 bytes.

Application Layer

AspectUMSHMeshCore
Payload typing1-byte payload type prefix4-bit payload type in header
Structured dataCoAP-over-UMSH (block-wise transfer)Multipart packets
Node identityIdentity payload with role, capabilities, name, options; advertisements authenticated by a frame signature, standalone identities by an embedded EdDSA signatureAdvertisement payload with public key, timestamp, EdDSA signature, appdata
URI schemeumsh:n:, umsh:ck:, umsh:cs:, coap-umsh://meshcore:// (contacts and channels)
Amateur radioOperator/station callsign options, explicit unencrypted modeNot defined

UMSH’s payload types identify which higher-layer protocol is carried inside the payload—whether UMSH-defined (text messages, chat rooms, node identity) or third-party (CoAP, 6LoWPAN). The MAC layer treats all payloads identically. MeshCore’s payload types define application-level semantics directly at the protocol level, without a clean separation between MAC and application concerns. UMSH defines a CoAP-over-UMSH transport (payload type 7) that inherits CoAP’s block-wise transfer for payloads larger than a single LoRa frame. MeshCore defines a multipart packet type for segmented transfers at the protocol level.

Layer Separation

AspectUMSHMeshCore
Protocol scopeMAC layer with cleanly separated application protocolsCombined MAC, network, and application layer
Payload interpretationOpaque at MAC layer—application protocols defined separatelyProtocol defines payload types with application semantics (text messages, advertisements, login, etc.)
FragmentationDelegated to higher-layer protocols in the payloadMultipart packet type defined at protocol level
Node identity / advertisementsApplication-layer payload (see Node Identity)Protocol-level advertisement packet with mandatory fields
Time dependencyTimestamp-free—monotonic frame counters for replay protection (see Frame Counter)Hash-based duplicate cache at MAC layer; uses non-regressing UNIX timestamp values for advertisement freshness and login sequencing

UMSH maintains a clean boundary between the MAC layer and higher-layer protocols. The MAC layer defines framing, addressing, encryption, authentication, and forwarding, and treats payload content as opaque. UMSH also defines its own application-layer protocols (text messaging, chat rooms, node identity, node management), but these are architecturally separate from the MAC layer and carried in the payload alongside any other higher-layer protocol.

MeshCore takes a more vertically integrated approach: the protocol directly defines payload types for text messages, node advertisements, login sequences, and multipart transfers without a clear separation between MAC and application concerns.

Timestamps and Time Dependency

MeshCore uses UNIX timestamp values in several protocol-critical roles:

  • Replay protection: MeshCore uses a fixed-size circular buffer of packet hashes (160 entries) for short-term duplicate detection. Once the buffer wraps, previously seen packets can no longer be detected as duplicates. Application-layer timestamps provide additional protection for some message types, but there is no MAC-layer replay protection counter.
  • Advertisement freshness: Node advertisements carry a timestamp. For an already-known contact, the receiver accepts an advertisement only if its timestamp is greater than the last accepted value from that contact.
  • Login sequencing: Login handling compares the sender’s timestamp with the last accepted value from that sender.

UMSH is entirely timestamp-free at the MAC layer. Replay protection is based on monotonic 4-byte frame counters (see Frame Counter), which require no clock synchronization and no access to absolute time. Higher-layer payloads (such as the node identity payload in Node Identity) may optionally carry timestamps for application-level purposes, but the MAC layer neither requires nor interprets them.

UMSH’s monotonic frame counter provides cryptographic replay protection that does not depend on a clock and does not degrade as traffic volume increases. MeshCore’s hash-based duplicate cache provides short-term deduplication but has a fixed capacity—in a busy mesh, the 160-entry buffer can wrap quickly, allowing replayed packets to be accepted after the original entry is evicted. MeshCore’s advertisement and login checks require a sender’s timestamp values not to regress relative to previously accepted state. Clock reset or rollback can therefore cause valid traffic to be rejected, but these checks do not require nodes’ clocks to be accurate or synchronized with one another.

Packet Overhead Comparison

Minimum overhead for a typical encrypted unicast message (no options, no flood hop count):

FieldUMSH (S=0, 16B MIC)UMSH (S=0, 4B MIC)UMSH (S=1)MeshCore
Header/FCF1111
Path length———1
Destination3331
Source33321
Security info555—
MAC/MIC1644–162
ECB block padding———0–15 (avg ~8)
Total overhead281645–57~14

UMSH supports MIC sizes of 4, 8, 12, and 16 bytes (see Security & Cryptography). With a 4-byte MIC and S=0, UMSH’s 16 bytes of overhead is 2 bytes more than MeshCore’s effective ~14 bytes—the additional cost of 3-byte source and destination hints compared to MeshCore’s 1-byte addresses, in exchange for uniform 1-in-16,777,216 collision resistance on both source and destination.

MeshCore’s use of AES-128-ECB requires the plaintext to be padded to a multiple of 16 bytes. This wastes 0–15 bytes per packet depending on the payload size, averaging roughly 8 bytes of dead space. When this padding overhead is included, MeshCore’s effective overhead rises from 6 bytes to approximately 14 bytes.

MeshCore achieves low per-packet overhead by using 1-byte addresses, a 2-byte MAC, and no frame counter or security control field. However, this compactness comes at a significant cost to security (ECB mode, 2-byte MAC, no replay protection counter, no key separation) and to flexibility (no first-contact without ANON_REQ, no blind unicast, no composable options).

UMSH with S=0 and a 16-byte MIC provides the strongest security configuration at 28 bytes of overhead. With shorter MICs, UMSH can trade integrity margin for payload capacity—a 4-byte MIC still provides a 1-in-2^32 forgery resistance (compared to MeshCore’s 1-in-2^16 with its 2-byte MAC) while matching MeshCore’s effective overhead.

Power Consumption

Address hint width has a direct effect on power consumption in battery-constrained LoRa nodes.

When a node receives a packet, it checks the destination hint against its own address before committing to cryptographic verification. If the hint matches, the node must attempt full packet verification to confirm whether the packet is genuinely addressed to it. Pairwise keys are cached after first contact, so no ECDH is needed for known senders—but verification still requires decrypting the payload with AES-CTR (using a CTR IV derived from the transmitted MIC) and then computing S2V over the decrypted plaintext to confirm the MIC matches. Only a collision from a completely unknown sender transmitting with a full 32-byte source key (S=1) would additionally require ECDH and HKDF derivation. If the hint does not match, the packet can be discarded immediately with minimal CPU cost.

The problem is collisions. In a network of many nodes, some fraction of packets addressed to other nodes will collide with your own hint and trigger unnecessary cryptographic work. The collision rate depends on the hint width:

ProtocolDestination hint widthFalse-positive rate per unicast packet
MeshCore8 bits (1 byte)~1 in 256
UMSH24 bits (3 bytes)~1 in 16,777,216

UMSH’s 3-byte node hints reduce spurious cryptographic wake-ups by a factor of ~65,536 compared to MeshCore, at a combined address overhead of 6 bytes per unicast packet versus MeshCore’s 2 bytes. In a busy mesh where a node receives hundreds of packets per hour intended for others, fewer wasted verifications means less CPU time, fewer memory accesses, and a faster return to sleep.

The same logic applies to multicast channel identifiers. MeshCore uses a 1-byte hash of the channel key, giving a 1-in-256 chance that a packet sent to a different channel collides with one of the receiver’s configured channel identifiers and triggers an unnecessary verification attempt. UMSH’s 2-byte channel identifier reduces this to 1-in-65536.

Repeater Power

Both protocols use flood routing, so nodes configured as repeaters must receive and retransmit packets intended for other nodes. Transmit is the most power-intensive radio operation, so minimizing unnecessary retransmissions matters. In practice, repeating is enabled only on dedicated infrastructure nodes; end-user devices are typically configured as non-repeating nodes and incur no forwarding transmit cost.

For dedicated repeater nodes, UMSH does not require decrypting or verifying the payload MIC before forwarding—the MAC layer treats payloads opaquely, and forwarding decisions use routing metadata, duplicate suppression, and configured forwarding policy. In particular, region filtering consists of comparing the packet’s fixed two-byte region-code options with pre-derived local codes; it does not require an HMAC over the payload for every configured region. UMSH’s channel access mechanisms (CAD with random backoff, SNR-based contention windows) reduce collisions, and forwarding confirmation with retries provides reliable hop-by-hop delivery. The signal-quality filtering options (minimum RSSI and SNR) allow senders to prevent retransmission over weak links, avoiding wasted transmit power on paths unlikely to deliver the packet successfully.

MeshCore’s SNR-based retransmit delay implicitly prioritizes better-positioned repeaters—nodes with stronger reception retransmit sooner, which can suppress weaker nodes via duplicate detection. MeshCore does not drop a packet merely because its received signal quality is poor, but that does not mean every repeater forwards every flood. Forwarding remains subject to path and duplicate limits, configured region policy, and transport-code matching. For a transport-scoped packet, a repeater performs the payload HMAC described above for each candidate region key and drops the packet if none match. Eligible repeaters still lack UMSH’s explicit sender-selected signal-quality thresholds, which can prevent retransmission over weak links entirely.

What This Means in Practice

The protocol differences described above translate into concrete user-visible behaviors:

  • Multi-hop delivery is more reliable. UMSH defines hop-by-hop forwarding confirmation with retries, so each repeater along the path confirms receipt before the previous hop moves on. Versions of this are present for both source-routed paths and flooded packets in UMSH. MeshCore’s forwarding is fire-and-forget in both flood and direct modes—if a transmission is lost at any hop, there is no recovery mechanism at the forwarding layer, leaving application-level retries as the only recovery mechanism on MeshCore.

  • Replay protection does not depend on clocks. MeshCore uses non-regressing UNIX timestamp values for advertisement freshness and login sequencing, so a clock reset or rollback can cause valid advertisements or login attempts to be rejected until the timestamp advances beyond the previously accepted value. The clocks do not need to be accurate or synchronized. UMSH is timestamp-free at the MAC layer—its replay protection instead uses monotonic frame counters.

  • Fewer false wake-ups on busy networks. MeshCore’s 1-byte address hints mean that roughly 1 in 256 packets addressed to other nodes will appear to match yours, triggering unnecessary cryptographic verification before the packet can be discarded. UMSH’s 3-byte hints reduce this to roughly 1 in 16 million, which matters for battery-powered nodes that need to return to sleep quickly.

  • Routes can combine source routing and flooding. A UMSH packet can be source-routed to a known region and then flood locally from there, reaching a specific area without flooding the entire mesh. MeshCore treats flood and direct routing as mutually exclusive.

  • Amateur radio operation is a first-class concern. UMSH defines packet options for operator and station callsigns and an explicit unencrypted mode, allowing compliant operation on amateur radio frequencies where encryption is prohibited and station identification is required. MeshCore does not define equivalent mechanisms.

  • Easier to extend without breaking existing deployments. UMSH’s composable options and opaque payload model let developers add new routing behaviors, packet options, or application protocols without changing the MAC layer—existing nodes simply ignore options and payload types they do not recognize. MeshCore’s vertically integrated design, where application-layer semantics are defined at the protocol level, means that new features are more likely to require coordinated firmware updates across the network.

  • Stronger security from a coherent cryptographic design. MeshCore’s cryptographic choices—AES-128-ECB encryption, 2-byte MACs, a 160-entry duplicate cache for replay protection, and no key separation—individually and collectively weaken the security guarantees the protocol can offer. Identical messages produce identical ciphertext, MAC forgery is feasible to brute-force, replayed packets are accepted once the duplicate cache wraps, and the same key material is reused across cryptographic operations. UMSH addresses all of these with AES-CTR encryption using a synthetic IV, configurable MIC sizes up to 16 bytes, monotonic frame counters for replay protection, HKDF-derived domain-separated keys, and optional perfect forward secrecy sessions that protect past traffic if a long-term key is later compromised. Something like this cannot be easily bolted onto MeshCore’s current design.

Summary of Design Differences

MeshCore optimizes aggressively for minimal packet overhead in the common case, accepting significant cryptographic and flexibility tradeoffs to maximize payload capacity within LoRa frame constraints. It takes a vertically integrated approach, defining application-layer payload types and relying on a fixed-size duplicate cache for deduplication and UNIX timestamps for advertisement freshness.

UMSH prioritizes cryptographic robustness, composable routing, privacy features, and strict layer separation, accepting higher overhead in exchange for stronger security guarantees and a more extensible protocol structure. By restricting itself to the MAC layer and using monotonic frame counters instead of timestamps, UMSH avoids coupling to specific application assumptions or clock synchronization requirements. The S flag allows the overhead to scale based on whether the receiver already knows the sender’s public key, bridging the gap for established communication pairs while still supporting zero-prior-state first contact.

Comparison with Meshtastic

This section compares UMSH with Meshtastic, a popular open-source LoRa mesh project. The comparison is based on the exact Meshtastic firmware tag v2.7.26.54e0d8d and its pinned protobuf definitions.

Note

This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!

Meshtastic and UMSH occupy different positions in the design space. Meshtastic is a mature, widely deployed application-focused system optimized for ease of use and broad hardware support. UMSH prioritizes authenticated communication, compact encoding, and explicit separation between its MAC and application protocols. The comparison below describes technical differences without implying that one set of tradeoffs is universally better than the other.

Identity and Addressing

AspectUMSHMeshtastic
Identity basis32-byte Ed25519 public key32-bit node number, normally initialized from the low 32 bits of a platform hardware identifier or MAC-like address; may be randomly reassigned after a detected collision
Cryptographic identityPublic key is the addressCurve25519 keypair generated and used for PKC where supported; not used for addressing
Source address in packetsCompact 3-byte hint (S=0) or 32-byte key (S=1)4-byte node number (cleartext)
Destination address3-byte hint4-byte node number or broadcast value (cleartext)
Channel identifier2-byte derived hint1-byte XOR of channel-name bytes and effective key bytes
Address spoofing resistanceCryptographic for pairwise unicast because keys are derived from public-key addressesNone for channel traffic; PKC authenticates against a stored or verified public key, but the node number is not cryptographically bound to that key

UMSH identifies nodes by their Ed25519 public keys, which serve as both the address and the cryptographic credential. Possession of the corresponding private key is required to authenticate and decrypt pairwise traffic. Meshtastic’s normal initial node number comes from a platform hardware identifier, but this is not universally a Bluetooth MAC and collision handling can assign a random replacement. The node number itself is not cryptographically bound to a key.

On supported, non-amateur builds, Meshtastic normally generates a Curve25519 keypair and uses PKC automatically for eligible unicast traffic when the peer key is known. A learned public key is associated with a node number and later mismatches are rejected; users can also verify keys explicitly. This provides conditional sender authentication for PKC traffic, but it remains a stored or verified association rather than an address derived from the key. Channel-encrypted traffic has no per-node authentication regardless of whether PKC keys are configured.

Packet Structure

AspectUMSHMeshtastic
Header1-byte FCF with version, type, flags16-byte fixed header (always cleartext)
Payload encodingRaw bytes with 1-byte payload type prefixProtobuf-encoded Data message
Packet types8 (via 3-bit field in FCF)Implicit in protobuf portnum field (30+ registered values)
Routing infoCoAP-style composable optionsFixed fields: hop limit (3-bit), next hop (1 byte), relay node (1 byte)
Flood hop countSplit 4-bit FHOPS field (max 15)Mandatory 3-bit field (encoded maximum 7)
Max LoRa payloadApproximately 255 bytes255-byte frame; the Data.payload schema permits 233 bytes, but usable application capacity depends on the encoded Data fields and security mode
Common unicast overhead relative to application body17 bytes with compact source and 4-byte MIC, or 29 bytes with 16-byte MIC; excludes salt, FHOPS, and optionsApproximately 22–23 bytes for channel encryption or 34–35 bytes for PKC, depending on protobuf length encoding

Before counting UMSH’s 1-byte payload-type prefix, FHOPS, or packet options, the fixed fields of a compact-source secured unicast occupy 16–30 bytes depending on MIC length and salt presence. A full 32-byte source raises that range to 45–59 bytes. The common-case row above adds the payload-type prefix and selects no salt with either a 4-byte or 16-byte MIC.

UMSH uses a compact 1-byte Frame Control Field with optional expansion: fields are present only when needed for the packet type. Meshtastic uses a fixed 16-byte header on every packet, with source and destination node numbers, packet ID, flags, channel hash, and routing fields always present.

Meshtastic’s header is always transmitted in cleartext, exposing sender node number, destination field, packet ID, and channel hash to any passive observer. For unicast, the destination field identifies the recipient; for broadcast, it contains the broadcast value rather than an individual recipient. UMSH’s addressing fields are compact hints that do not directly reveal a complete node identity, and blind unicast and encrypted multicast conceal their address fields as defined below.

Meshtastic encodes application payloads in the schema-based, extensible protobuf Data envelope. UMSH uses raw byte payloads with a 1-byte type prefix, minimizing encoding overhead while leaving structure to the selected application protocol.

Cryptography

AspectUMSHMeshtastic (channel)Meshtastic (eligible unicast PKC)
EncryptionAES-256-CTR (AES-SIV, RFC 5297)AES-128-CTR or AES-256-CTR, selected by effective PSK length; the default effective key is 16 bytesAES-CCM
AuthenticationS2V (AES-CMAC), 4/8/12/16-byte MICNone8-byte CCM authentication tag
Key exchangeX25519 ECDHPre-shared keyCurve25519 ECDH
Key derivationHKDF-SHA256 with domain separationEffective PSK used directlySHA-256 of ECDH shared secret
Nonce constructionFrame counter + optional salt in SECINFOPacket ID + sender node numberPacket ID + 4-byte random contribution + sender node number
Duplicate/replay handling4-byte monotonic frame counter with replay windowPartially randomized 32-bit packet ID and finite duplicate cacheSame packet-ID duplicate cache as channel traffic
Forward secrecyOptional PFS sessionsNoNo

Channel Encryption

The most significant cryptographic difference is that Meshtastic’s channel-encrypted packets have no authentication. AES-CTR provides confidentiality but no integrity protection. This means:

  • Ciphertext can be modified in transit without a cryptographic integrity check. When an attacker knows or can predict the corresponding plaintext, CTR bit flipping can be targeted without knowing the key.
  • Any node with the channel key can forge packets claiming to be from another node, since channel mode has no per-node authentication and the sender’s node number in the cleartext header is not cryptographically bound to the ciphertext.

UMSH authenticates every secured packet with an S2V MIC (4–16 bytes; see MIC Size Selection Guidance). Even with a 4-byte MIC, UMSH provides 2^-32 forgery resistance, which is qualitatively different from the absence of an integrity check on Meshtastic channel traffic. A UMSH channel packet cannot be modified in transit or injected by a non-member without detection.

The second bullet above, however, describes a property UMSH multicast shares, and it is not a difference between the two protocols. A shared symmetric key proves channel membership but cannot distinguish one member from another, so a UMSH channel member can likewise claim another member’s source address; see Multicast Sender Authentication. What an unsigned UMSH channel packet provides is integrity and membership authentication against outsiders, not per-sender attribution within the channel. Attribution to a specific sender requires unicast, where pairwise keys bind the source, or a frame signature, which a sender can add to any multicast.

PKC for Eligible Unicast Traffic

Meshtastic uses Curve25519 ECDH with AES-CCM for eligible unicast traffic, providing confidentiality and authentication against the public key associated with the claimed sender node number. It is not limited to text direct messages. The firmware considers PKC automatically for locally originated, non-broadcast application traffic when keys are available, except for position, node info, routing, traceroute, amateur operation, and certain serial/GPIO cases. Broadcast traffic such as position, telemetry, and channel text remains in channel mode and therefore unauthenticated.

UMSH applies the same S2V-based construction to secured unicast and multicast, so there is no secured traffic class left unauthenticated and no separate authenticated mode to opt into. Broadcast packets carry no SECINFO or MIC; an unsigned broadcast makes no claim of authenticity, while one carrying a Frame Signature option is attributable to its sender’s key.

Key Derivation

Meshtastic uses the effective channel PSK directly as the AES key for channel encryption. A configured one-byte PSK is not a one-byte AES key: it is a public shorthand alias expanded by the firmware into an effective 16-byte key. For PKC, the ECDH shared secret is hashed with SHA-256 to produce the AES key.

UMSH uses HKDF-SHA256 with domain-separated labels to derive independent encryption and authentication keys from each shared secret. This prevents cross-purpose key reuse and follows standard cryptographic practice.

Routing

AspectUMSHMeshtastic
Flood routingYes, bounded by flood hop countYes (managed flood with SNR-based priority)
Source routingYes, via source-route optionNo
Source route followed by flood tailYes, both can be carried in the same packetNo; learned next-hop delivery can fall back to flooding, but there is no source-route field
Next-hop routingN/AYes (learned from ACK paths, v2.6+)
Hop budget15 flood hops plus source-routed hops3-bit encoded budget (max 7); selected favorite-router infrastructure hops may preserve the budget
Duplicate detectionMIC cacheFinite cache keyed by sender and partially randomized 32-bit packet ID
Forwarding confirmationHop-by-hop retries with backoff for source-routed hops; retries at a flood originReliable (want_ack) origin: initial transmission plus at most two retransmissions; directed intermediate: initial transmission plus at most one retransmission; intermediate flood relays do not retry
Channel accessCAD with random backoff; SNR-based contention windowsCAD with random backoff; channel-utilization-based local contention and SNR-based flood contention
Signal-quality controlsMin RSSI and min SNR eligibility optionsLower-SNR/farther flood candidates tend to transmit earlier; infrastructure roles also affect priority
Region-scoped floodingRegion code optionNot defined
TracerouteTrace option on any packet, with per-hop signalDedicated TRACEROUTE_APP port

Both protocols use flood-based routing as their primary discovery and fallback mechanism. Meshtastic’s managed flood gives lower-SNR receptions smaller contention windows, treating apparent distance from the sender as a heuristic for useful forwarding progress. ROUTER nodes receive a separate early-transmission advantage. Most roles cancel a queued rebroadcast after overhearing a duplicate, while infrastructure roles such as ROUTER may intentionally transmit anyway. UMSH instead provides explicit signal-quality thresholds that allow the sender to control flood-relay eligibility per packet.

Meshtastic’s 3-bit field encodes a maximum hop budget of 7, but this is not an unconditional cap on the physical number of relays. After the first hop, selected transitions between configured ROUTER, ROUTER_LATE, or CLIENT_BASE infrastructure nodes can preserve the budget when the previous relay is a favorite. UMSH’s 4-bit flood hop count permits up to 15 flood hops, while source-routed hops consume packet space rather than flood budget.

Meshtastic v2.6+ added next-hop routing for unicast: after a successful ACK exchange, the firmware learns which relay carried the response and uses that relay as the designated next hop for subsequent packets. UMSH achieves directed delivery through source-route options learned via traces. The recipient can cache the accumulated trace directly as a source route for subsequent communication with the sender because the trace is already built most-recent hop first; see Route Learning.

Both protocols perform channel activity detection and randomized contention. Meshtastic sizes normal local-send contention from recent channel utilization and uses received SNR for flood-rebroadcast contention. UMSH uses CAD with random backoff and SNR-based contention windows as defined by its channel-access rules.

Both protocols provide forwarding confirmation, but the scopes differ. For a Meshtastic want_ack transmission, the origin makes at most three total attempts: the initial transmission and two retransmissions. When a learned next hop is in use, the final origin attempt falls back to flooding. A directed intermediate relay also listens for its designated next hop and may retransmit once; intermediate flood relays do not retry because no single next hop is designated. UMSH applies passive forwarding confirmation and retry to each source-routed hop and to a flood origin, while intermediate flood relays likewise do not retry.

Privacy

AspectUMSHMeshtastic
Header confidentialityAddressing fields are compact hints; blind modes encrypt source/destinationHeader always cleartext: sender, destination field, packet ID, and channel hash exposed
Source concealmentEncrypted multicast, blind unicastNot supported
Destination concealmentBlind unicastNot supported
Node ID linkabilityPublic key; ephemeral keys are supportedNormally persistent hardware-derived node number, with possible random collision reassignment
Anonymous first contactEphemeral Ed25519 key with S=1 flagNot supported

Meshtastic’s 16-byte cleartext header exposes the complete sender node number and destination field on every packet. A unicast packet therefore exposes both node numbers; a broadcast exposes the sender and the broadcast destination value. A passive observer can still correlate sender node numbers, build traffic graphs, and track a normally persistent identifier over time without the channel key.

UMSH’s compact hints reveal less information directly, and blind unicast and encrypted multicast modes conceal their defined source and/or destination fields. Nodes can also use ephemeral keypairs for anonymous communication.

Multicast and Group Communication

AspectUMSHMeshtastic
Channel key size32 bytesConfigured as 0, 1-byte public alias, 16, or 32 bytes; effective AES key is 0, 16, or 32 bytes
Channel identifier2-byte derived hint1-byte XOR of channel-name bytes and effective key bytes
Channels per nodeUnlimited (implementation-defined)Up to 8
Multi-hop multicastYes (flood with flood hop count)Yes (managed flood with hop limit)
Group message authChannel-key-based S2V MICNone (AES-CTR only)
Source privacySource encrypted when encryption enabledNo (source in cleartext header)
Named channelsYes (key derived from name)Yes (name + PSK configured together)

Both protocols support multiple channels with independent keys. Meshtastic limits a node to 8 configured channels. Under an independent uniform-candidate model, a specific unrelated channel has a 1-in-256 chance of matching Meshtastic’s 1-byte hint and a 1-in-65536 chance of matching UMSH’s 2-byte identifier. With m independent configured candidates, the probability of at least one match is 1-(255/256)^m for Meshtastic and 1-(65535/65536)^m for UMSH. Meshtastic’s XOR construction is not a cryptographic hash, so these are modeling assumptions rather than exact operational rates. A matching Meshtastic hint causes the receiver to try the corresponding channel key and validate the resulting protobuf.

Application Layer

AspectUMSHMeshtastic
Payload typing1-byte payload type prefixProtobuf portnum field (30+ registered values)
Payload encodingRaw bytesProtocol Buffers or port-specific binary/text formats
Structured dataCoAP-over-UMSH (block-wise transfer)Protobuf with defined message schemas
Text messagingUMSH text message payloadTEXT_MESSAGE_APP (portnum 1)
Position/telemetryNot defined (delegated to higher-layer protocols)Built-in POSITION_APP, TELEMETRY_APP
Node identityIdentity payload with role, capabilities, nameNODEINFO_APP with User protobuf
Remote administrationNode management MAC commandsADMIN_APP (portnum 6)
AudioNot definedAUDIO_APP (codec2, 2.4 GHz only)
Store and forwardNot definedSTORE_FORWARD_APP; Store-and-Forward++ schema for native Linux nodes
Other registered integrationsHigher-layer protocols can be assigned payload typesTAK/ATAK (including V2), Reticulum, LoRaWAN bridge, Cayenne, and remote-shell wire schema, among others
Amateur radioOperator/station callsign options, explicit unencrypted modeis_licensed state plus dedicated ham-mode command; callsign in long name, automatic PSK/admin removal, and licensed relay restrictions
ImplementationProtocol spec (language-agnostic)C++ firmware + protobuf definitions and companion applications

Meshtastic defines a rich application ecosystem with built-in firmware support, companion-side integrations, and registered wire schemas for position sharing, telemetry, waypoints, audio, store-and-forward, TAK, Reticulum, remote shell, and other uses. Registration of a PortNum and schema does not imply that every firmware build contains a native handler for that application.

UMSH defines a smaller set of application protocols (text messaging, chat rooms, node identity, node management) and delegates richer application functionality to higher-layer protocols carried in the payload, such as CoAP. This is less feature-complete out of the box but allows UMSH to carry arbitrary higher-layer content without changing the MAC protocol.

Both protocols address amateur radio operation, but at different levels. Meshtastic’s dedicated ham-mode command sets the callsign as the node’s long name, sets the short name and radio overrides, raises the NodeInfo cadence required for identification, changes rebroadcasting to local-only, and automatically removes channel PSKs and admin access. Setting the raw is_licensed owner flag also clears encryption automatically, but the operator must separately ensure the callsign fields are populated. Licensed nodes refuse to relay packets to or from nodes known to be unlicensed. The callsign is carried in the existing user-info field rather than a dedicated per-packet protocol field.

UMSH defines dedicated packet options for operator and station callsigns, and its security-control field explicitly indicates whether encryption is enabled. In licensed-only mode, monitoring software can verify the presence of the required callsign option and the absence of encryption directly from the packet. Both protocols still require the operator to select and configure the lawful operating mode.

Layer Separation

AspectUMSHMeshtastic
Protocol organizationMAC layer with separately specified application protocolsShared firmware and protobuf registry spanning radio integration, routing control, and applications
Forwarding-time payload interpretationOpaque at the MAC layerClear raw header is sufficient for forwarding; encrypted Data may remain opaque to a relay
FragmentationDelegated to higher-layer protocolsNo generic mesh-layer fragmentation; individual applications or transports may define their own
Application registrationApplication protocols are architecturally separate30+ application identifiers registered in the core PortNum schema

UMSH specifies an explicit boundary between its MAC layer and application protocols: the MAC treats payloads opaquely and can carry any assigned higher-layer protocol. Meshtastic likewise has a concrete wire boundary between its 16-byte raw forwarding header and the encrypted protobuf Data envelope, so a relay can forward traffic it cannot decrypt. Its Data schema also contains port, payload, response-correlation, and some routing-related fields, and its application identifiers live in the shared core protobuf registry. The result is greater integration between routing, firmware modules, and application dispatch than in UMSH, but not an absence of all layer boundaries.

Neither protocol defines generic fragmentation at the mesh/MAC layer. UMSH explicitly delegates it to higher-layer protocols such as CoAP block-wise transfer. Meshtastic applications and companion transports can define their own mechanisms; for example, the Reticulum tunnel port is specified to carry fragmented RNS packets.

Timestamps and Time Dependency

AspectUMSHMeshtastic
Replay/duplicate handling4-byte monotonic frame counter with replay windowPartially randomized 32-bit packet ID and finite duplicate cache
Timestamps in packetsNone at MAC layerrx_time metadata (not transmitted in the LoRa header and not used for routing decisions)
Clock synchronization requiredNoNo

Neither protocol requires clock synchronization for core operation. Meshtastic records reception timestamps as local metadata but does not use them for routing or duplicate suppression. UMSH provides replay protection with monotonic frame counters and a receive window. Meshtastic suppresses recent duplicates by caching the sender and partially randomized packet ID; because that cache is finite, this should not be described as durable cryptographic replay protection.

Packet Overhead Comparison

The following compares overhead relative to the application body for a common secured unicast message. The UMSH columns use a compact source, no salt, no FHOPS, and no packet options. The Meshtastic columns assume a common one-byte portnum value and the locally originated Data.bitfield; the protobuf envelope is 6 bytes while the application body is shorter than 128 bytes and normally 7 bytes once its length requires a two-byte varint.

FieldUMSH (S=0, 16B MIC)UMSH (S=0, 4B MIC)Meshtastic (channel)Meshtastic (PKC)
Header/FCF111616
Destination33(in header)(in header)
Source33(in header)(in header)
SECINFO55——
Payload-type prefix11——
MIC164——
Protobuf Data envelope——6–76–7
PKC tag + random nonce contribution———12
Total overhead291722–2334–35

Meshtastic channel mode derives its AES-CTR nonce from the cleartext packet ID and sender node number and transmits no separate channel nonce. PKC uses those header fields plus a transmitted 4-byte random contribution and carries an 8-byte CCM tag, adding exactly 12 bytes beyond an otherwise equivalent channel Data envelope.

UMSH with a 16-byte MIC has 29 bytes of application-body-to-wire overhead under the stated conditions, compared with approximately 22–23 bytes for Meshtastic channel mode. UMSH’s total includes authentication that Meshtastic channel mode lacks. With a 4-byte MIC, UMSH uses 17 bytes under the same conditions while still providing a truncated authentication tag.

The Meshtastic Data.payload field is sized for as many as 233 bytes, but that schema capacity is not a universal application-body limit. For a common locally originated text packet, the encoded port, payload length, and required bitfield make about 232 bytes fit in channel mode; the additional 12 PKC bytes reduce the analogous PKC capacity to about 220 bytes. Other Data fields or larger portnum encodings reduce it further.

Power Consumption

Power consumption on a battery-constrained LoRa node is influenced by radio airtime, receive duty cycle, retransmission behavior, and CPU work after a packet is received. The protocol and firmware establish which operations occur, but their practical energy magnitude requires measurement on representative hardware.

Channel Filtering and False Positives

For channel traffic, a compact channel identifier is a pre-crypto filter. If an unrelated incoming channel produces the same identifier as one configured locally, the receiver must try the local candidate key to determine whether the packet is valid.

ProtocolChannel identifier widthUniform per-candidate match probability
Meshtastic8 bits (XOR of channel name and effective key)1 in 256
UMSH16 bits (derived hint)1 in 65536

Under independent uniform assumptions, the per-candidate probability differs by a factor of 256. With m configured candidates, the probability of at least one match is 1-(255/256)^m for Meshtastic and 1-(65535/65536)^m for UMSH. Meshtastic’s XOR construction is not a uniform cryptographic hash in every real configuration, so these figures are a model rather than measured traffic rates.

The Meshtastic receive loop confirms the mechanism: every configured channel whose hint matches triggers an AES-CTR decryption and protobuf parse attempt until one succeeds or the candidates are exhausted. This is additional CPU work, but no measurement cited here establishes a meaningful battery-life impact, and the AES cost may be small compared with LoRa receive airtime.

Unicast Filtering

For unicast packets, Meshtastic’s 4-byte cleartext node number provides an exact destination comparison before payload decryption. UMSH’s 3-byte destination hint has a modeled 1-in-16,777,216 per-identity false-positive probability. A collision requires cryptographic processing to determine whether the packet is actually addressed to the receiver; pairwise keys can be cached after first contact, so known-sender handling does not require a fresh ECDH operation.

This is a protocol tradeoff: Meshtastic spends one additional cleartext destination byte and exposes the complete unicast node number, while UMSH uses a shorter, less identifying prefilter with a small collision probability. The practical power significance of that difference depends on traffic and hardware and is not asserted here.

Packet Length and Airtime

Meshtastic’s fixed 16-byte header is transmitted on every packet. UMSH includes only fields required by the packet type. Longer LoRa packets consume more airtime and receiver-on time, but the total comparison depends on UMSH’s selected MIC, optional fields, and source form as well as on Meshtastic’s security mode and protobuf envelope.

Forwarding Power

Both protocols use flooding, so forwarding nodes may receive and retransmit packets. Transmit is normally among the most power-intensive radio operations. Meshtastic defaults to the CLIENT role with rebroadcast mode ALL, so ordinary client nodes participate in managed rebroadcasting. A node that should not forward must use CLIENT_MUTE, rebroadcast mode NONE, or another appropriately restrictive configuration. The old REPEATER role is deprecated; current infrastructure configurations include roles such as ROUTER, ROUTER_LATE, and CLIENT_BASE.

Meshtastic’s managed flood gives lower-SNR receptions shorter expected delays, allowing nodes that appear farther from the sender to relay earlier. Most client-like roles cancel a queued transmission after overhearing a duplicate. ROUTER nodes receive separate early priority and intentionally do not cancel every duplicate. These policies reduce or redirect redundant transmissions compared with an unsuppressed flood, but the effect depends on topology and role configuration.

UMSH’s minimum-RSSI and minimum-SNR options allow the original sender to set explicit flood-forwarding thresholds. A repeater below the threshold does not retransmit the packet. Meshtastic’s approach chooses contention priority automatically; UMSH’s approach makes relay eligibility explicit but requires the sender to choose suitable thresholds.

UMSH repeaters do not need to decrypt or verify the application payload before forwarding because the MAC layer treats it opaquely. Meshtastic forwarding likewise uses the clear raw header and can relay both channel and PKC ciphertext without decrypting the protobuf Data body.

Summary of Design Differences

Meshtastic is a full-featured mesh communication system with a large and active user community. It provides a rich application ecosystem, broad hardware support, and an easy on-ramp for non-technical users. Its channel-based encryption model is simple to configure and deploy, while eligible unicast traffic can use PKC when peer keys are available.

UMSH prioritizes authenticated secured traffic, compact encoding, metadata-concealment modes, and explicit MAC/application boundaries. Secured unicast and multicast packets carry authentication; unsigned broadcast packets intentionally make no authenticity claim, and any packet can carry a frame signature that attributes it to its sender.

Key tradeoffs:

  • Authentication: UMSH authenticates every secured unicast and multicast packet. Meshtastic channel traffic has no integrity check; eligible PKC unicast traffic is authenticated against the stored or verified peer key.
  • Privacy: UMSH provides compact addressing hints and opt-in blind modes. Meshtastic exposes the complete sender and destination fields in every cleartext header; unicast therefore exposes both endpoint node numbers, while broadcast uses the broadcast destination value.
  • Identity model: UMSH uses cryptographic public keys as addresses. Meshtastic uses a normally hardware-derived node number that is not cryptographically bound to its PKC key.
  • Overhead: Under the conditions in the overhead table, UMSH uses 17 bytes with a 4-byte MIC or 29 with a 16-byte MIC; Meshtastic uses approximately 22–23 bytes for channel mode or 34–35 for PKC. UMSH’s totals include authentication, while Meshtastic channel mode does not.
  • Application richness: Meshtastic provides a much richer built-in and companion application ecosystem. UMSH delegates richer functionality to higher-layer protocols.
  • Protocol organization: UMSH specifies MAC and application protocols separately. Meshtastic has a distinct raw forwarding header and encrypted application envelope, while application registration, routing control, and firmware modules share a common protobuf and firmware ecosystem.
  • Implementation: Meshtastic is a mature C++ firmware with broad device support. UMSH is not tied to an implementation language or runtime, and its compact wire design can target bare-metal microcontrollers.
  • Ease of deployment: Meshtastic is designed for immediate use with consumer hardware. UMSH requires implementation effort and explicit key configuration.

Comparison with Reticulum

This section compares UMSH with Reticulum, a cryptography-based networking stack designed for operation over a wide range of mediums, including LoRa. The comparison is based on Reticulum v1.1.4 (tag 1.1.4) and its manual and source code.

Note

This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!

The Reticulum claims in this document can be independently verified against the following source files:

FileRelevant claims
RNS/Reticulum.pyMTU, header sizes, announce bandwidth cap, IFAC derivation
RNS/Packet.pyPacket types, header layout, context values
RNS/Identity.pyKey sizes, ECDH, HKDF derivation, ephemeral keys, ratchet system, announce format
RNS/Destination.pyDestination types, destination hash construction, GROUP limitations, ratchet interval
RNS/Transport.pyRouting, replay protection via packet hash cache, max hop cap, IFAC generation
RNS/Cryptography/Token.pyAES-256-CBC, PKCS7, HMAC-SHA256, IV handling, timestamp removal
RNS/Link.pyLINK session ECDH, symmetric key persistence, MTU signalling, link modes
RNS/Interfaces/Interface.pyLink MTU auto-configuration (optimise_mtu)
RNS/Discovery.pyOn-network interface discovery, network identity system

Protocol Scope

The most fundamental difference between UMSH and Reticulum is their scope.

UMSH defines a MAC layer with cleanly separated application protocols. The MAC layer handles framing, addressing, encryption, authentication, and forwarding, and treats payloads opaquely. Application protocols (text messaging, chat rooms, node management) are architecturally separate and carried in the payload alongside any other higher-layer content such as CoAP or 6LoWPAN.

Reticulum is a complete network stack that replaces the IP layer entirely. It provides addressing, routing, link establishment, encryption, reliable delivery (via its Resources API), request/response patterns, and bidirectional channels. Reticulum does not separate MAC-layer concerns from application-layer concerns; these are interwoven throughout the stack.

This difference has practical implications: UMSH’s simpler MAC-layer model has lower minimum state requirements, making it easier to implement on constrained microcontrollers. Reticulum’s richer protocol machinery (path tables, link establishment, announce propagation) demands more from an implementation regardless of language. UMSH’s MAC layer can carry arbitrary higher-layer protocols, while Reticulum applications must use Reticulum’s own APIs for structured communication.

Identity and Addressing

AspectUMSHReticulum
Identity key32-byte Ed25519 public key64-byte keyset: 32-byte X25519 + 32-byte Ed25519 (Identity.py:58, KEYSIZE = 256*2)
Address in packetscompact 3-byte hint (S=0) or full 32-byte key (S=1)16-byte truncated SHA-256 hash (Reticulum.py:146, TRUNCATED_HASHLENGTH = 128)
Source address3-byte hint (S=0) or full 32-byte key (S=1)Not included (no source address in packets)
Destination address3-byte destination hint16-byte destination hash
Channel identifier2-byte derived hint16-byte destination hash

UMSH identifies nodes directly by their Ed25519 public keys and uses compact 3-byte hints as prefilters for efficient matching. The S flag allows including the full 32-byte key when needed (first contact, ephemeral keys). Reticulum derives 16-byte destination hashes via a two-step construction (Destination.py:118): (1) the aspect name (a dotted string like app.sensor.temperature) is hashed with SHA-256 and truncated to 10 bytes (Identity.py:80, NAME_HASH_LENGTH = 80); (2) the final address is SHA-256(name_hash || identity_hash)[:16], where identity_hash = SHA-256(public_key)[:16]. These hashes are larger than UMSH hints but serve a different purpose—they are meant to be globally unique identifiers rather than prefilters.

Reticulum does not include a source address in any packet. This provides initiator anonymity by default but means that the recipient must already have context (via an established link or a prior announce) to know who sent a given packet. UMSH includes the source address (as a compact hint or full key) in every packet, which allows stateless first-contact and simplifies protocol logic at the cost of revealing the sender’s identity to observers. UMSH offers two opt-in mechanisms that reduce identity exposure. Blind unicast encrypts the source address with the channel key so that only channel members can identify the sender. PFS sessions use ephemeral node addresses for the duration of the session, so an observer sees only hints derived from short-lived keys rather than the nodes’ long-term identities—the long-term identity hints never appear on the wire during the session. This identity obscuration is not unconditional: because the PFS handshake is authenticated with the nodes’ long-term keys, an attacker who later compromises a long-term private key can retroactively attribute the session to those identities, even though the session’s content remains protected.

Packet Structure

AspectUMSHReticulum
Header1-byte FCF with version, type, flags2-byte header (flags + hop count)
Packet types8 (via 3-bit field in FCF)4: DATA, ANNOUNCE, LINKREQUEST, PROOF (Packet.py:60–63)
Destination typesImplicit in packet type (unicast, multicast, broadcast, blind)4: SINGLE, GROUP, PLAIN, LINK (Destination.py:63–66)
Routing infoCoAP-style composable optionsTransport ID field (16 bytes) in HEADER_2
Flood hop countSplit 4-bit FHOPS field (max 15)Mandatory 1-byte field
MTULoRa frame size (typically 255 bytes)500-byte network MTU (Reticulum.py:91, MTU = 500); per-link MTU discovery (since v0.9.3) allows upward negotiation on capable links
Typical unicast overhead (total)14–28 bytes (depending on MIC size and source hint vs full key)~91 bytes LINK / ~108 bytes SINGLE (19–35 byte header + 56–88 bytes crypto; see Packet Overhead Comparison)

UMSH uses compact fields that are present only when needed for the packet type. Reticulum’s 16-byte destination hashes and optional 16-byte transport IDs result in larger headers (19–35 bytes before crypto overhead), and the total per-packet overhead—header plus cryptographic fields—ranges from roughly 91 to 108 bytes depending on destination type. On a 255-byte LoRa frame, this leaves 147–164 bytes for payload compared to UMSH’s 227–239 bytes.

Reticulum’s 500-byte network MTU exceeds what most LoRa configurations can carry in a single frame. Reticulum introduced link MTU discovery in v0.9.3, which allows adjacent nodes to negotiate a higher effective MTU than 500 bytes when the underlying interface can support it—but this only applies to high-bandwidth interfaces. The Interface.optimise_mtu() method (Interface.py:115–138) maps link speed to hardware MTU, and sets HW_MTU = None for any interface running at or below 62,500 bps—which encompasses every LoRa configuration. When HW_MTU is None, the link request falls back to signalling the base 500-byte MTU (Link.py:273–278), and link MTU discovery is never entered. For LoRa interfaces, Reticulum relies on the RNode firmware to reassemble sub-255-byte air frames into 500-byte packets before presenting them to the stack via KISS. UMSH is designed to fit within a single LoRa frame, avoiding fragmentation entirely.

UMSH uses composable CoAP-style options for routing metadata (source routes, traces, signal-quality thresholds, region codes), allowing packets to carry exactly the routing information they need. Reticulum uses a fixed two-header-type system: HEADER_1 for direct packets and HEADER_2 for transport-routed packets, with no equivalent to UMSH’s composable options.

Cryptography

AspectUMSHReticulum
EncryptionAES-256-CTR (AES-SIV, RFC 5297)AES-256-CBC with PKCS7 padding (Token.py:91); AES-128 support removed in v1.0.0
AuthenticationS2V (AES-CMAC), 4/8/12/16-byte MICHMAC-SHA256 (32-byte tag, Token.py:50, TOKEN_OVERHEAD = 48)
Key exchangeX25519 ECDHX25519 ECDH (Identity.py:581)
Key derivationHKDF-SHA256, domain-separated (K_enc, K_mic)HKDF-SHA256, 64-byte output split into HMAC key + AES key (Identity.py:86, DERIVED_KEY_LENGTH = 512//8)
Nonce handlingSIV construction (CTR IV derived from the MIC)Random 16-byte IV per packet (Token.py:89, os.urandom(16))
Replay protection4-byte monotonic frame counterDuplicate packet hash detection (Transport.py:59, packet_hashlist)
Per-packet overhead (crypto)5–7 bytes (SECINFO) + 16 bytes (MIC) = 21–23 bytes16 bytes (IV) + 32 bytes (HMAC) = 48 bytes minimum (LINK); +32 bytes ephemeral pubkey for SINGLE
Forward secrecyOptional PFS sessions via MAC commands (per-session ephemeral keys)Per-packet ephemeral key for SINGLE (Identity.py:574); optional ratchets for LINK (default min 30 min rotation, Destination.py:90, up to 512 ratchet keys stored per destination, 30-day expiry)
Future modes—AES-256-GCM defined (link mode 0x02) but reserved; OTP/post-quantum modes reserved

Encryption Mode

UMSH uses AES-SIV (RFC 5297), in which the synthetic IV serves as both the MIC and the source of the CTR IV, providing nonce-misuse resistance. Reticulum uses AES-256-CBC with a random 16-byte IV. Both are sound constructions—the primary difference in practice is overhead: CBC requires transmitting a 16-byte IV and adds 1–16 bytes of PKCS7 padding, while UMSH’s SIV construction derives the IV from the MIC (which is already transmitted) and uses CTR mode which requires no padding.

Authentication and Integrity

UMSH’s S2V MIC is configurable at 4, 8, 12, or 16 bytes (see MIC Size Selection Guidance), providing 32-bit to 128-bit integrity protection. Reticulum’s 32-byte HMAC-SHA256 tag provides 256-bit integrity. Both are sound choices; UMSH’s configurable size allows deployments to trade integrity margin for payload capacity within the constrained LoRa frame budget.

Key Management

For unicast, UMSH derives stable pairwise keys from the ECDH shared secret via HKDF with domain-separated labels. These keys are reused across packets, with per-packet variability provided by the frame counter and optional salt in SECINFO. This is efficient: no per-packet key exchange overhead. For forward secrecy, UMSH defines PFS sessions in which both nodes exchange ephemeral node addresses via a two-message handshake and communicate using session-specific keys for an agreed duration. Compromise of long-term keys does not expose traffic encrypted under PFS session keys. PFS sessions add no per-packet overhead once established.

Reticulum uses two approaches. For SINGLE (one-off) destinations, each packet includes a fresh ephemeral X25519 public key (32 bytes), providing per-packet forward secrecy at substantial overhead cost—32 extra bytes on every packet (Identity.py:574, ephemeral_key = X25519PrivateKey.generate()). For LINK (session) destinations, a single ECDH exchange establishes symmetric keys that persist for the link’s lifetime—similar to UMSH’s stable pairwise keys (Link.py:340, self.shared_key = self.prv.exchange(self.peer_pub)). Reticulum also offers a ratchet mechanism that rotates keys at a configurable minimum interval (default 30 minutes, adjustable per-destination via Destination.set_ratchet_interval() (Destination.py:514)), providing periodic forward secrecy within a link (Destination.py:90, RATCHET_INTERVAL = 30*60). Up to 512 ratchet keys are stored per destination, each expiring after 30 days. Ratchet key presence is signalled via a context flag in announce packets, allowing senders to use the most recent ratchet key in place of the static identity key, providing forward secrecy for single-packet communication without the full 32-byte ephemeral pubkey overhead.

Both protocols offer forward secrecy, but with different granularity and overhead tradeoffs. Reticulum’s SINGLE mode provides per-packet forward secrecy at 32 bytes per packet; UMSH’s PFS sessions provide per-session forward secrecy at zero per-packet overhead after setup.

Replay Protection

UMSH uses explicit 4-byte monotonic frame counters, which provide deterministic, stateless replay detection with a well-defined forward window. A receiver can immediately reject a replayed packet by comparing the counter to its stored state.

Reticulum detects duplicates by caching packet hashes (Transport.py:59, packet_hashlist = set()). This approach works but has different tradeoffs: it requires maintaining a hash cache, and once the cache fills it is evicted in bulk via a two-generation rolling scheme—when the active set exceeds 500,000 entries it is moved into a packet_hashlist_prev set (Transport.py:60) and a fresh set starts accumulating (Transport.py:565–567, cap: Transport.py:115, hashlist_maxsize = 1000000). A replayed packet that was seen in neither the current nor the previous generation would not be detected. The reference implementation’s 500,000-entry threshold assumes ample memory; on constrained hardware where the cache must be significantly smaller, the window for undetected replay narrows accordingly.

Cryptographic Overhead

For an encrypted unicast message, the total cryptographic overhead differs significantly:

ComponentUMSH (16B MIC)UMSH (4B MIC)Reticulum (SINGLE)Reticulum (LINK)
SECINFO5 B5 B——
MIC / HMAC16 B4 B32 B32 B
IV——16 B16 B
Ephemeral pubkey——32 B—
CBC padding——1–16 B (avg ~8)1–16 B (avg ~8)
Subtotal (crypto)21 B9 B~88 B~56 B

UMSH supports MIC sizes of 4, 8, 12, and 16 bytes (see Security & Cryptography), allowing deployments to trade integrity margin for payload capacity. Even with a 16-byte MIC, UMSH’s 21 bytes of crypto overhead is far less than Reticulum’s 56–88 bytes.

Routing

AspectUMSHReticulum
Flood routingYes, bounded by flood hop countYes (broadcast propagation)
Source routingYes, via source-route optionNo
Hybrid routingSource route + flood hop count in same packetNo
Transport/directed routingN/ATransport nodes with next-hop forwarding
Path discoveryTrace option on any flooded packetAnnounce flooding + path request/response
Max hops15 flood + unlimited source-routed128 (hard-coded announce propagation cap, Transport.py:41, PATHFINDER_M)
Forwarding confirmationYes (retries with backoff)No—forwarding is fire-and-forget; reliability is end-to-end via cryptographic proofs
Channel accessCAD with random backoff; SNR-based contention windowsNot defined at protocol level; RNode firmware implements CSMA with persistence probability for LoRa interfaces
Signal-quality filteringMin RSSI and min SNR optionsNot defined at protocol level
Region-scoped floodingRegion code optionNot defined
Announce bandwidth capNot defined (implementation policy)Default 2% of interface bandwidth (Reticulum.py:115, ANNOUNCE_CAP = 2), configurable per-interface via announce_cap key
Interface discoveryNot definedOn-network auto-discovery via rnstransport.discovery.interface destination (since v1.1.0)

UMSH and Reticulum take fundamentally different approaches to routing.

UMSH provides source routing—the sender can specify the exact sequence of repeaters a packet should traverse, using 2-byte router hints. This can be combined with flood routing: a packet can be source-routed to a specific area and then flood locally. Path discovery is built into the MAC layer via the Trace option, which accumulates router hints as a packet floods—the recipient caches the accumulated trace as a source route for all subsequent communication with the sender (see Route Learning).

UMSH defines channel access mechanisms (CAD with random backoff, SNR-based contention windows) and forwarding confirmation with retries, providing reliable hop-by-hop delivery and collision avoidance. Reticulum does not define equivalent mechanisms at the protocol level, though RNode firmware provides CSMA with persistence probability for LoRa interfaces independently of Reticulum.

Reticulum uses next-hop routing via Transport Nodes—dedicated forwarding nodes that maintain path tables learned from announces. Regular nodes do not forward packets. When no path is known, a path request is flooded (51 bytes in non-transport mode: 19-byte HEADER_1 + 16-byte destination hash + 16-byte request tag); transport nodes with cached paths respond. This approach is more automatic but requires designated infrastructure nodes and does not support sender-specified routing.

Reticulum’s maximum announce propagation is 128 hops (Transport.py:41, PATHFINDER_M). This constant is hard-coded; the hop count field is one byte and could technically carry values up to 255, but transport logic enforces the 128-hop limit.

Reticulum v1.1.0 introduced on-network interface discovery: nodes can broadcast structured discovery announces (containing interface type, LoRa parameters, IFAC credentials, and GPS coordinates) to the rnstransport.discovery.interface destination. Other nodes can receive these announces and automatically connect to trusted remote interfaces. This capability requires the LXMF module and uses proof-of-work stamps to prevent spam. UMSH does not define an equivalent mechanism.

UMSH’s signal-quality filtering (minimum RSSI and SNR options) allows packets to avoid weak links, which is valuable in LoRa networks where marginal links waste airtime on packets that are unlikely to be received reliably. Reticulum does not define equivalent mechanisms at the protocol level.

RNode and LoRa Access

Reticulum does not define radio-level concerns such as channel access or fragmentation at the protocol level. Instead, it relies on RNode, an open-source firmware for commodity ESP32-based LoRa boards, to bridge between the protocol stack and the LoRa physical layer. RNode communicates with the host via KISS framing over serial, Bluetooth LE, or TCP, and handles radio configuration, CSMA/CA channel access, and airtime regulation in firmware.

Critically for LoRa use, RNode also handles fragmentation: since Reticulum’s 500-byte network MTU exceeds the ~255-byte LoRa frame limit, the firmware transparently splits oversized packets across two LoRa frames and reassembles them on receive. This keeps the Reticulum stack simple—it sees a 500-byte pipe—but means that large packets require two air frames transmitted back-to-back, doubling airtime and introducing a window where interference or a collision on either frame loses the entire packet. Because fragmentation and channel access are handled in firmware, the protocol stack has no visibility into these concerns and cannot factor them into routing or scheduling decisions.

UMSH takes the opposite approach: each packet fits within a single LoRa frame, and channel access (CAD with random backoff, SNR-based contention windows) is defined at the protocol level. This allows any radio interface to be used without requiring firmware-level fragmentation or channel access logic, at the cost of bounding per-packet payload to what a single frame can carry.

Privacy and Anonymity

AspectUMSHReticulum
Source address in packetsYes (compact hint or 32-byte key)No (default initiator anonymity)
Blind unicastYes (source encrypted with channel key)N/A (no source to conceal)
Multicast source concealmentYes (source encrypted inside ciphertext)N/A
Anonymous first contactEphemeral Ed25519 key with S=1 flagPer-packet ephemeral key for SINGLE destinations
Destination concealmentNot definedNot defined
Interface access controlNot definedIFAC (truncated Ed25519 signature per packet)
Network trust domainsNot definedNetwork Identity system (since v1.1.0)

The two protocols achieve privacy through different structural choices.

Reticulum omits the source address from all packets, providing initiator anonymity as a default property of the protocol. The tradeoff is that recipients must establish context through other means (announces, link establishment) before they can identify who is communicating with them.

UMSH includes source addresses by default but provides explicit privacy modes. Blind unicast encrypts the source address with a channel key so that only channel members can identify the sender. Encrypted multicast conceals the source inside the ciphertext. These are opt-in features that allow nodes to choose their privacy posture per packet.

Reticulum’s IFAC (Interface Access Code) mechanism provides network-level access control: a shared 64-byte keypair (X25519 + Ed25519) is derived from the network name and/or passphrase via HKDF-SHA256(SHA-256(network_name) || SHA-256(passphrase)). Each packet is signed with the Ed25519 key, and a configurable-length tail of that signature (1–64 bytes) is appended to the packet as the IFAC code (Transport.py:1485–1490). Interfaces reject packets with invalid IFAC codes. UMSH does not define an equivalent mechanism.

Reticulum v1.1.0 introduced a Network Identity system: a standard Reticulum identity keypair can be designated as a network’s signing authority. Network Identity keys sign interface discovery announces, allowing receiving nodes to verify that a discovered interface belongs to a trusted administrative domain. This enables optional encrypted discovery and provides a foundation for inter-network trust and future distributed name resolution. UMSH does not define an equivalent network identity layer.

Reticulum v1.1.0 also introduced a distributed blackhole list: specific identities can be blacklisted, causing their announces to be dropped by participating nodes. The blackhole list can be published and updated across the network. UMSH has no equivalent mechanism.

Multicast and Group Communication

AspectUMSHReticulum
Channel key size32 bytes32 bytes (AES-256)
Channel identifier2-byte derived hint16-byte destination hash
Multi-hop multicastYes (flood with flood hop count)No (single-hop broadcast only)
Group message authChannel-key-based S2V (16-byte MIC)Channel-key-based HMAC-SHA256 (32-byte tag)
Source privacySource encrypted when encryption enabledNo source address to conceal
Named channelsYes (key derived from name)Not defined

UMSH supports multi-hop multicast via flood forwarding with flood hop count limits. Reticulum’s GROUP destinations are currently limited to single-hop broadcast—the manual states:

Packets to this type of destination are not currently transported over multiple hops, although a planned upgrade to Reticulum will allow globally reachable group destinations.

For LoRa mesh networks that rely on multi-hop coverage, this is a notable difference.

Application Layer

AspectUMSHReticulum
Payload typing1-byte payload type prefix1-byte context field (21 defined values; Packet.py:72–92)
Structured dataCoAP-over-UMSH (block-wise transfer)Resources API (multi-packet reliable transfer)
Node identityIdentity payload with role, capabilities, name, options; advertisements authenticated by an Ed25519 frame signatureAnnounce packets with public key, name hash, app data, Ed25519 signature (Identity.py:355, validate_announce())
Service discoveryBeacon broadcastsAspect-based naming + announce propagation
Interface discoveryNot definedOn-network auto-discovery with trust verification (since v1.1.0)
Network identityNot definedSigning authority keypair for administrative domains (since v1.1.0)
Amateur radioOperator/station callsign options, explicit unencrypted modeNot defined
ImplementationsProtocol spec (language-agnostic); experimental Rust reference implementationPython 3 reference; C++ for microcontrollers; Rust

Reticulum’s protocol is documented in its manual. In addition to the Python reference implementation, Reticulum has community C++ and Rust ports targeting embedded platforms. UMSH’s Rust reference implementation is experimental and early in development.

UMSH delegates reliable multi-packet transfer to CoAP’s block-wise transfer mechanism, reusing a well-established standard. Reticulum provides its own Resources API for the same purpose, including compression, sequencing, and checksumming—capable but specific to the Reticulum stack.

Reticulum v1.1.0 introduced structured interface discovery at the application layer: nodes can publish and consume typed discovery records that include interface parameters, GPS coordinates, IFAC credentials, and network identity signatures. This enables a form of self-organizing network management that has no counterpart in UMSH, which relies on out-of-band coordination for infrastructure configuration.

Timestamps and Time Dependency

Both protocols are designed to operate without clock synchronization.

AspectUMSHReticulum
Replay protection4-byte monotonic frame counterDuplicate packet hash cache
Timestamps in headersNoneNone (explicitly removed from Fernet-derived token format; Token.py:41–49)
Clock synchronization requiredNoNo

Both protocols avoid timestamp dependencies at the protocol level. UMSH uses monotonic frame counters for replay protection. Reticulum uses packet hash caching for duplicate detection. Neither requires nodes to agree on wall-clock time.

Reticulum’s ratchet mechanism uses local time for 30-day key expiry and minimum rotation intervals, but this is a local policy decision rather than a protocol requirement—no timestamp is transmitted on the wire.

Packet Overhead Comparison

Minimum overhead for a typical encrypted unicast message with no routing options (no source route, no flood hops, no trace). In practice, multi-hop packets will carry additional routing metadata—UMSH adds 1 byte for flood hops and 2 bytes per source-route hop; Reticulum adds 16 bytes for the Transport ID when routed through a transport node:

FieldUMSH (S=0, 16B MIC)UMSH (S=0, 4B MIC)Reticulum (SINGLE)Reticulum (LINK)
Header/FCF1122
Destination331616
Transport ID———16 (if routed)
Source33——
Context byte——11
SECINFO55——
Ephemeral pubkey——32—
IV——1616
MIC / HMAC1643232
CBC padding——1–161–16
Total overhead2816~108~91

On a 255-byte LoRa frame:

UMSH (S=0, 16B MIC)UMSH (S=0, 4B MIC)Reticulum (SINGLE)Reticulum (LINK)
Available payload227 B239 B~147 B~164 B

With a 16-byte MIC, UMSH provides roughly 45–55% more payload capacity than Reticulum. With a 4-byte MIC, UMSH’s 16 bytes of total overhead leaves 239 bytes for payload—over 45% more than Reticulum on a typical LoRa frame. At LoRa data rates where every byte costs airtime, these differences affect how much payload capacity remains for application data.

Reticulum’s 500-byte network MTU exceeds what most LoRa configurations can transmit in a single frame, so Reticulum requires link-layer fragmentation at the LoRa interface that further reduces effective throughput. Link MTU discovery (added in v0.9.3) allows Reticulum to negotiate larger MTUs on capable links, but provides no relief for LoRa interfaces with sub-500-byte physical limits.

Power Consumption

The power profiles of UMSH and Reticulum differ across every dimension: platform, per-packet overhead, and filtering behavior.

Minimum Hardware Floor

Reticulum’s protocol machinery—path tables for Transport Node operation, 500-byte packet buffers, announce propagation state, and link establishment sessions—requires more RAM and processing than UMSH’s simpler model, raising the minimum hardware floor regardless of implementation language. A higher hardware floor generally means a higher baseline power draw, since more capable hardware tends to consume more power even when idle.

UMSH also requires per-node state—own keypair, configured channel keys, per-peer cached keys and frame counters, a duplicate cache, and optionally cached source routes—but the single-frame design avoids the need for large packet buffers, and the simpler protocol machinery fits on lower-power microcontrollers.

False-Positive Filtering

Reticulum’s 16-byte (128-bit) destination addresses have an essentially zero collision probability—a node receiving a packet addressed to someone else will never mistake it for its own. There is no wasted cryptographic work from address false positives. UMSH’s 3-byte destination hints have a ~1-in-16,777,216 false-positive rate; when a collision occurs, the node must attempt full packet verification before discarding it. Pairwise keys are cached after first contact, so no ECDH is needed for known senders—but verification still requires decrypting the payload with AES-CTR (using a CTR IV derived from the transmitted MIC) and then computing S2V over the decrypted plaintext to confirm the MIC matches. ECDH and HKDF would additionally be required for a false positive from an unknown sender transmitting with a full 32-byte source key (S=1), which is rare in normal operation. In practice, on a LoRa network with modest traffic, spurious collisions are rare enough that this cost is negligible.

Packet Length and Airtime

Reticulum’s total per-packet overhead is roughly 91 bytes (LINK) or 108 bytes (SINGLE), combining header and cryptographic fields. UMSH’s total overhead is 14–28 bytes for a typical authenticated unicast. Shorter packets mean less airtime per message, which translates directly to less receive power for every node in range—a cost the sender imposes on the whole network.

Fragmentation

Reticulum’s 500-byte network MTU exceeds the ~255-byte LoRa frame limit, requiring link-layer fragmentation for larger messages. A node receiving a fragmented message must keep its radio and MCU active across multiple frames until reassembly completes. UMSH is designed to fit each packet into a single LoRa frame, so the radio can return to sleep as soon as one frame is processed.

Announce Traffic and Repeater Power

Reticulum relies on periodic announce flooding to build and maintain path tables. Transport Nodes re-broadcast announces on all interfaces (subject to a default 2% bandwidth cap); regular nodes receive announces but do not re-broadcast them. This creates a continuous background of traffic that all nodes must receive and that Transport Nodes must retransmit. UMSH does not define an equivalent mechanism—path discovery is on-demand via the Trace option and imposes no standing overhead.

For data packet forwarding, Reticulum divides nodes into two classes at the protocol level: regular nodes, which do not forward unicast packets, and Transport Nodes, which maintain path tables and forward on behalf of others. This means most nodes in a Reticulum network incur zero transmit cost for forwarding unicast traffic. UMSH makes the same distinction at the configuration level—repeating is enabled only on dedicated infrastructure nodes, and end-user devices are typically configured as non-repeating. The practical power implication for non-repeating nodes is the same in both protocols; the difference is that Reticulum enforces the separation in the protocol itself rather than leaving it to deployment configuration.

The infrastructure dependency is the key tradeoff: Reticulum’s routing model requires Transport Nodes to be present and reachable, and these nodes need reliable power to maintain path tables. UMSH’s flood-based model works without any fixed infrastructure, with repeater nodes sharing the forwarding load.

Summary of Design Differences

Reticulum is a comprehensive, general-purpose network stack designed to operate across a wide range of mediums—from gigabit Ethernet to sub-kilobit LoRa. It prioritizes medium independence, automatic path discovery, initiator anonymity by default, and a rich application API. Recent versions (v1.1.0+) have added on-network interface discovery and a network identity system that enable more sophisticated network management and trust hierarchies.

UMSH is purpose-built for constrained LoRa networks. It prioritizes compact packet encoding, minimal overhead, composable routing options, and strict layer separation. Its small per-packet overhead, single-frame design, and lower protocol state requirements (no path tables, no mandatory announce propagation, no clock synchronization, no global packet hash cache) are designed with constrained hardware in mind.

Key tradeoffs:

  • Overhead: UMSH achieves 60–85% lower per-packet overhead than Reticulum (depending on MIC size), maximizing payload capacity within tight LoRa frame budgets.
  • Cryptographic overhead: UMSH’s SIV construction avoids transmitting a separate IV and requires no padding; Reticulum’s CBC mode (AES-256 only since v1.0.0) adds 17–32 bytes of IV and padding overhead per packet.
  • Routing flexibility: UMSH offers composable source routing, hybrid routing, and signal-quality filtering. Reticulum offers automatic next-hop routing via transport nodes, and on-network interface auto-discovery since v1.1.0.
  • Privacy model: Reticulum provides initiator anonymity by default (no source address), plus a network identity system for administrative trust domains. UMSH provides source addresses by default with opt-in privacy modes.
  • Multicast: UMSH supports multi-hop multicast. Reticulum’s group communication is currently single-hop only (multi-hop planned).
  • Replay protection: UMSH uses per-peer monotonic frame counters, which require a small amount of per-peer state but provide deterministic replay detection regardless of traffic volume. Reticulum uses a global packet hash cache, which requires no per-peer state but must be large enough to cover the replay window—a tradeoff that favors hosts with ample memory.
  • Scope: Reticulum is a complete network stack with reliable delivery, sessions, and application APIs. UMSH is a MAC layer with defined-but-separate application protocols, designed to carry arbitrary higher-layer content.
  • Implementation complexity: Reticulum’s richer protocol machinery (path tables, link establishment, announce propagation) raises the minimum implementation complexity. UMSH’s simpler MAC-layer model is more amenable to constrained implementations.

Test Vectors

This appendix contains byte-level packet examples. All values are hexadecimal, and multi-byte numeric fields are big-endian.

Conventions

The generated examples use fixed Ed25519 private keys so the appendix covers the full path from private key to public key to X25519 ECDH to packet bytes.

  • Node A private key: 1112 1314 1516 1718 191A 1B1C 1D1E 1F20 2122 2324 2526 2728 292A 2B2C 2D2E 2F30
  • Node A public key: ED54 A59F B1AC 3A51 2393 5136 2941 B868 E85A 60E3 D7B2 485D 8288 21DC 7A69 C279
    • Source hint: ED 54 A5
  • Node B private key: 3132 3334 3536 3738 393A 3B3C 3D3E 3F40 4142 4344 4546 4748 494A 4B4C 4D4E 4F50
  • Node B public key: 6C28 FD05 8C18 C88C 6CCE 2AF9 81D2 D11C 851B 123E D5B6 9B78 7677 3ED0 99EA 3F83
    • Destination hint: 6C 28 FD
  • Pairwise shared secret: 5ADD 834F C109 FAD5 2F04 1C5A F84A 7966 526D 364D 1895 AFFC D794 E044 F3A9 DB14
    • Derived from the two private keys via the implementation’s Ed25519-to-X25519 conversion and X25519 ECDH.
  • Channel key: 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A 5A5A
  • Derived channel identifier: B0 8D

FCF Bit Layout Reference

  7   6   5   4   3   2   1   0
+-------+-----------+---+---+---+
| VER   | PKT TYPE  | S | R | H |
+-------+-----------+---+---+---+

SCF Bit Layout Reference

  7   6   5   4   3   2   1   0
+---+-------+---+---+-----------+
| E |  MIC  | S | D | RESERVED  |
+---+-------+---+---+-----------+

Example 1: Broadcast Beacon (S=0)

A minimal beacon with a 3-byte source hint and no payload.

FieldValueHex
FCFVER=3, TYPE=0 (broadcast), S=0, R=0, H=0C0
SRCNode A hintED 54 A5
C0 ED 54 A5

Total: 4 bytes.

Example 2: Broadcast Beacon (S=1)

A first-contact beacon carrying the sender’s full 32-byte public key.

FieldValueHex
FCFVER=3, TYPE=0 (broadcast), S=1, R=0, H=0C4
SRCNode A full keyED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2 79
C4 ED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8
68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2
79

Total: 33 bytes.

Example 3: Encrypted Unicast (S=0)

An encrypted unicast from Node A to Node B using source hints and frame counter 42.

FieldValueHex
FCFVER=3, TYPE=2 (unicast), S=0, R=0, H=0D0
DSTNode B hint6C 28 FD
SRCNode A hintED 54 A5
SCFE=1, MIC=3 (16-byte), S=0E0
Frame Counter4200 00 00 2A
PayloadEncrypted 48 65 6C 6C 6F ("Hello")8B 60 67 EB 6C
MIC16 bytesF6 D2 39 C9 05 12 41 ED 8C A4 C2 D8 89 1A 34 58
D0 6C 28 FD ED 54 A5 E0 00 00 00 2A FF 8B 60 67
EB 6C F6 D2 39 C9 05 12 41 ED 8C A4 C2 D8 89 1A
34 58

Total: 34 bytes.

Example 4: Encrypted Unicast with Ack Requested (S=1)

A first-contact encrypted unicast from Node A to Node B requesting a MAC acknowledgement. The full 32-byte source key is included.

FieldValueHex
FCFVER=3, TYPE=3 (unicast ack-req), S=1, R=0, H=0DC
DSTNode B hint6C 28 FD
SRCNode A full keyED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2 79
SCFE=1, MIC=3 (16-byte), S=0E0
Frame Counter100 00 00 01
PayloadEncrypted 68 65 79 ("hey")4C A9 EB
MIC16 bytesFE A5 FD 44 7A 2F E0 FF D5 57 B5 4E E6 E8 91 81
DC 6C 28 FD ED 54 A5 9F B1 AC 3A 51 23 93 51 36
29 41 B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC
7A 69 C2 79 E0 00 00 00 01 FF 4C A9 EB FE A5 FD
44 7A 2F E0 FF D5 57 B5 4E E6 E8 91 81

Total: 61 bytes.

Example 5: Encrypted Multicast (E=1)

An encrypted multicast from Node A on channel B08D. The encrypted body contains the source hint followed by the plaintext payload.

FieldValueHex
FCFVER=3, TYPE=4 (multicast), S=0, R=0, H=0E0
CHANNELDerived channel identifierB0 8D
SCFE=1, MIC=3 (16-byte), S=0E0
Frame Counter500 00 00 05
Encrypted dataENCRYPT(`SRC
MIC16 bytesF3 C4 8B 8F 96 EA A1 EF F6 19 47 E8 49 77 16 70
E0 B0 8D E0 00 00 00 05 FF D4 4B B6 0E 34 6D E5
F4 F3 C4 8B 8F 96 EA A1 EF F6 19 47 E8 49 77 16
70

Total: 33 bytes.

Example 6: Authenticated Multicast (E=0)

An authenticated but unencrypted multicast from Node A carrying payload type 03 followed by "Hello".

FieldValueHex
FCFVER=3, TYPE=4 (multicast), S=0, R=0, H=0E0
CHANNELDerived channel identifierB0 8D
SCFE=0, MIC=3 (16-byte), S=060
Frame Counter300 00 00 03
SRCNode A hintED 54 A5
Payload`03
MIC16 bytes14 0E 9E 52 53 A7 3F A6 58 3C 0C 94 C1 5E EA 9F
E0 B0 8D 60 00 00 00 03 FF ED 54 A5 03 48 65 6C
6C 6F 14 0E 9E 52 53 A7 3F A6 58 3C 0C 94 C1 5E
EA 9F

Total: 34 bytes.

Example 7: Encrypted Unicast with Options and Flood Hops

An encrypted unicast with a region code option, an empty trace option, and flood hop limit 4.

Options encoding:

OptionNumberDeltaLengthEncoding
Trace77070
Region Code114242 then value 78 53
End marker———FF
FieldValueHex
FCFVER=3, TYPE=2 (unicast), S=0, R=0, H=1D1
FHOPSFHOPS_REM=4, FHOPS_ACC=040
DSTNode B hint6C 28 FD
SRCNode A hintED 54 A5
SCFE=1, MIC=3 (16-byte), S=0E0
Frame Counter1000 00 00 0A
OptionsTrace + region code + end marker70 42 78 53 FF
PayloadEncrypted 68 65 79 ("hey")B7 C4 59
MIC16 bytesEC 72 BD EA E9 59 DB 86 BA AD 14 9C 92 5F B4 0B
D1 40 6C 28 FD ED 54 A5 E0 00 00 00 0A 70 42 78
53 FF B7 C4 59 EC 72 BD EA E9 59 DB 86 BA AD 14
9C 92 5F B4 0B

Total: 37 bytes.

Example 8: Blind Unicast (S=0)

A blind unicast on channel B08D. The destination hint and source hint are encrypted together in ENC_DST_SRC, while the payload is encrypted with the blind-unicast payload keys.

FieldValueHex
FCFVER=3, TYPE=6 (blind unicast), S=0, R=0, H=0F0
CHANNELDerived channel identifierB0 8D
SCFE=1, MIC=3 (16-byte), S=0E0
Frame Counter700 00 00 07
ENC_DST_SRCENCRYPT(`DST
ENC_PAYLOADENCRYPT("Hello")B7 B1 EE 01 4B
MIC16 bytesAF 4C 3E 68 BD 61 82 47 53 98 47 CF F9 0C 28 70
F0 B0 8D E0 00 00 00 07 FF 1D 7D 4C CE 9C 9E B7
B1 EE 01 4B AF 4C 3E 68 BD 61 82 47 53 98 47 CF
F9 0C 28 70

Total: 36 bytes.

Example 9: Contact Exchange Derivations

Contact exchange values, taking Node A’s public key as the asking device’s session key S_A and Node B’s as the asked device’s S_B.

  • N_A: 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F
  • N_B: 1011 1213 1415 1617 1819 1A1B 1C1D 1E1F
ValueResult
COMMITMENTB0B0 5CA5 750B 0DC9 02D5 E7B8 66DC 81D6 3DDE 8BDD 70E0 1A0F 8B50 BF5F FA66 9E7D
Code digest, first four bytesD0 43 62 18
CODE076952
HUE of S_A61, color D0A84B
HUE of S_B119, color 53C69E

Colors at hue steps 0, 64, 128, and 192 are EC8DAB, CBAA4B, 3DC6B1, and 90AAFA.

A Hello from a T-LoRa Pager (model identifier 9), and A’s Request:

0A 01 00 00 09
0A 01 01 B0 B0 5C A5 75 0B 0D C9 02 D5 E7 B8 66
DC 81 D6 3D DE 8B DD 70 E0 1A 0F 8B 50 BF 5F FA
66 9E 7D

Example 10: MAC Ack

Node B’s acknowledgement of Example 4. ACK MIC is the first four bytes of Example 4’s MIC. ACK TAG is truncate_to_4(S2V(K_mic, S1 = C8 FF FF, S2 = V, S3 = ACK MIC)), where C8 FF FF is the ack’s AAD (its FCF and the terminator, with no static options) and V is Example 4’s full S2V output, which its 16-byte MIC carries whole.

FieldValueHex
FCFVER=3, TYPE=1 (MAC ack), S=0, R=0, H=0C8
ACK MICFirst 4 bytes of VFE A5 FD 44
ACK TAG4-byte MIC16 61 6C D1
C8 FE A5 FD 44 16 61 6C D1

Total: 9 bytes.

Example 11: MAC Ack with Operator Callsign

The acknowledgement of Example 10 as a Licensed-Only node sends it, carrying the operator callsign N0CALL. The callsign is a static option, so the ack’s AAD is C8, the option’s type-length-value encoding, then FF FF, and the tag differs from Example 10’s.

FieldValueHex
FCFVER=3, TYPE=1 (MAC ack), S=0, R=0, H=0C8
OptionsOperator Callsign (option 4), N0CALL44 5B BB 08 2C
ACK MICFirst 4 bytes of VFE A5 FD 44
ACK TAG4-byte MIC15 B2 56 E4
C8 44 5B BB 08 2C FE A5 FD 44 15 B2 56 E4

Total: 14 bytes.

Signed Frames

Examples 12 to 14 are signed with Node A’s Ed25519 key. Each shows its signature input: "UMSH-SIG", the frame’s AAD with a broadcast’s Frame Signature zeroed, the channel commitment for a multicast, and the plaintext the MIC’s S2 covers without the signature.

  • Channel commitment of the channel key above: B08D B3C1 400A 1C11 F1D1 464D D6BA 945F D037 2DDD 38CA 63A1 84D3 6FD1 11FE BA40

Example 12: Signed Authenticated Multicast (E=0, D=1)

An unencrypted multicast from Node A carrying its full key, signed. SRC || PAYLOAD || SIG is the MIC’s S2.

FieldValueHex
FCFVER=3, TYPE=4 (multicast), S=1, R=0, H=0E4
CHANNELDerived channel identifierB0 8D
SCFE=0, MIC=3 (16-byte), S=0, D=168
Frame Counter1200 00 00 0C
End of optionsNo optionsFF
SRCNode A public keyED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2 79
Payload`03
SIGEd25519 signature64 60 E7 36 CF 31 6B B3 2F 43 8C A2 81 DE 0D E0 2C 3B BE B7 A6 CB 19 5A 5C 60 55 BD 46 A9 F5 E7 53 2C 96 AE F7 CC E8 06 C7 F4 FC 46 2C D9 B8 61 CC 60 D4 47 65 F6 6E 4E 3A 5D DB 10 91 3D 6D 05
MIC16 bytes59 96 7A 9D 75 D9 FC DC AA 9F AD 4C 50 3B 2E 47
E4 B0 8D 68 00 00 00 0C FF ED 54 A5 9F B1 AC 3A
51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48
5D 82 88 21 DC 7A 69 C2 79 03 48 65 6C 70 64 60
E7 36 CF 31 6B B3 2F 43 8C A2 81 DE 0D E0 2C 3B
BE B7 A6 CB 19 5A 5C 60 55 BD 46 A9 F5 E7 53 2C
96 AE F7 CC E8 06 C7 F4 FC 46 2C D9 B8 61 CC 60
D4 47 65 F6 6E 4E 3A 5D DB 10 91 3D 6D 05 59 96
7A 9D 75 D9 FC DC AA 9F AD 4C 50 3B 2E 47

Total: 126 bytes.

Signature input (87 bytes):

55 4D 53 48 2D 53 49 47 E4 B0 8D 68 00 00 00 0C
FF FF B0 8D B3 C1 40 0A 1C 11 F1 D1 46 4D D6 BA
94 5F D0 37 2D DD 38 CA 63 A1 84 D3 6F D1 11 FE
BA 40 ED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41
B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69
C2 79 03 48 65 6C 70

Example 13: Signed Encrypted Multicast (E=1, D=1)

An encrypted multicast from Node A, signed. The signature is encrypted with the source hint and the payload.

FieldValueHex
FCFVER=3, TYPE=4 (multicast), S=0, R=0, H=0E0
CHANNELDerived channel identifierB0 8D
SCFE=1, MIC=3 (16-byte), S=0, D=1E8
Frame Counter1300 00 00 0D
End of optionsNo optionsFF
Encrypted dataENCRYPT(`SRC
MIC16 bytes02 BB 9A 22 FE F4 1A 6E 91 9D 34 72 41 AA 42 B9
E0 B0 8D E8 00 00 00 0D FF 69 2E 61 79 21 A2 08
42 BF 2E 1C D8 6F 88 A1 4A E7 EF 44 BC DF 11 34
EC 85 6F 93 06 39 96 0C 99 D6 70 79 87 08 EA A8
0E 61 BA DA 28 E2 B2 A8 58 97 D5 9C C2 8A 12 33
DB 1B 21 18 53 85 5E CE 11 00 79 F3 6C 99 1C 24
10 02 BB 9A 22 FE F4 1A 6E 91 9D 34 72 41 AA 42
B9

Total: 97 bytes.

Signature input (58 bytes):

55 4D 53 48 2D 53 49 47 E0 B0 8D E8 00 00 00 0D
FF FF B0 8D B3 C1 40 0A 1C 11 F1 D1 46 4D D6 BA
94 5F D0 37 2D DD 38 CA 63 A1 84 D3 6F D1 11 FE
BA 40 ED 54 A5 48 65 6C 6C 6F

Example 14: Signed Broadcast

A broadcast from Node A carrying its full key and a Frame Signature option (0) with counter 14. In the signature input the option’s 64 signature bytes are zeros.

FieldValueHex
FCFVER=3, TYPE=0 (broadcast), S=1, R=0, H=0C4
SRCNode A public keyED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2 79
Option headerDelta 0, length 68 (13 + 37)0D 37
Counter1400 00 00 0E
SIGEd25519 signature03 79 A8 FA 3A B8 6E 4F 59 C7 6F 0E E6 5C 76 6B D2 F1 03 38 0E 62 D9 AD 4E E8 66 E5 89 B3 56 C0 54 AC F8 50 AF D6 9A DD B7 A3 8B 7C 18 E8 77 A6 AA 55 84 68 F7 01 2F E0 91 24 4E 6B 89 52 BE 0C
End of optionsPayload followsFF
Payload"Hi"48 69
C4 ED 54 A5 9F B1 AC 3A 51 23 93 51 36 29 41 B8
68 E8 5A 60 E3 D7 B2 48 5D 82 88 21 DC 7A 69 C2
79 0D 37 00 00 00 0E 03 79 A8 FA 3A B8 6E 4F 59
C7 6F 0E E6 5C 76 6B D2 F1 03 38 0E 62 D9 AD 4E
E8 66 E5 89 B3 56 C0 54 AC F8 50 AF D6 9A DD B7
A3 8B 7C 18 E8 77 A6 AA 55 84 68 F7 01 2F E0 91
24 4E 6B 89 52 BE 0C FF 48 69

Total: 106 bytes.

Signature input (117 bytes):

55 4D 53 48 2D 53 49 47 C4 ED 54 A5 9F B1 AC 3A
51 23 93 51 36 29 41 B8 68 E8 5A 60 E3 D7 B2 48
5D 82 88 21 DC 7A 69 C2 79 00 00 00 44 00 00 00
0E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 FF FF 48 69