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
Sflag 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 and trace-route hints are 2 bytes. See Addressing.
- AES (Advanced Encryption Standard)
- A symmetric block cipher standardized by NIST. UMSH uses AES-128 in CTR mode for payload encryption and AES-CMAC for message authentication.
- AES-SIV (Synthetic Initialization Vector)
- A misuse-resistant authenticated encryption scheme (RFC 5297) in which the initialization vector is derived from an authentication tag computed over the plaintext. If the same plaintext is accidentally encrypted twice with the same key, the output reveals only that duplication — keys and other traffic remain uncompromised. UMSH uses a construction inspired by AES-SIV: AES-CMAC is used to compute the MIC, which then seeds AES-128-CTR for encryption.
- 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
- A node that relays 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: they consume source-route hints and forward packets as repeaters do, but their onward transmission cannot be observed on the inbound medium.
- 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-128 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 payload signatures, most notably for node identity broadcasts.
- 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 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. The protocol differentiates between source-routed hops and flood-routed hops.
- 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 keyed 4-byte ack tag derived from the original packet’s MIC and the pairwise encryption key, 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, computed using AES-CMAC 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 Route
- A packet option that instructs each forwarding repeater to prepend its router hint 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 trace route 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
Eflag. 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 7) — identifies the transmitting station, updated by repeaters during forwarding
- Region codes (packet option 11) — IATA airport codes 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
0xFFmarker, the string extends to the end of the payload (or to the start of a trailing signature).
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-route 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
Sflag in the FCF is clear, or - the full 32-byte public key, when the
Sflag 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 | MIC |
+-----+-------+----------+---------+-----------+---------+---------+-----+
1 B 0/1 B 0/2/3 B 0/3/32 B 0/5/7 B variable variable 0-16 B
Where:
FHOPSis present if the FCF flood hop count flag is setDSTis a 3-byte destination hint (MAC Ack packets carry noDST)CHANis a 2-byte channel identifierSRCis a compact 3-byte source hint (whenSflag is clear) or 32-byte source public key (whenSflag is set); in multicast and blind unicast packets with encryption enabled,SRCis encrypted inside the ciphertext rather than appearing as a separate fieldSECINFOis present on authenticated/encrypted packet typesMICis present on authenticated/encrypted packet types; MAC acks carry a fixed ack trailer (ack MIC + ack tag) instead
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 typeS= 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-zeroH= flood hop count present. Zero if direct. Cleared before the byte enters the AAD
Packet Type Values
| Value | Name |
|---|---|
| 0 | BCST: Broadcast |
| 1 | UACK: MAC Ack |
| 2 | UNIC: Unicast |
| 3 | UNAR: Unicast, Ack-Requested |
| 4 | MCST: Multicast |
| 5 | RESERVED |
| 6 | BUNI: Blind Unicast |
| 7 | BUAR: 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
FHOPScounts 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 hops; 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, which is used for MAC ack routing when no trace route 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 value | Meaning |
|---|---|
| 0–12 | Literal value |
| 13 | One extended byte follows; value = byte + 13 |
| 14 | Two extended bytes follow; value = uint16 (big-endian) + 269 |
| 15 | Reserved — 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).
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:
- Determine the options+payload region:
buf[options_start .. packet_end − trailer_len], wheretrailer_lenis the length of the fixed-size trailer at the end of the packet (MIC length from SECINFO, or 8 for the MAC ack trailer, or 0 for broadcast). The trailer length is known before options are parsed. - 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.
- If a
0xFFbyte is encountered during the scan, the bytes remaining in the region (between the0xFFand the trailer) are the payload. A0xFFwith zero bytes remaining is a valid empty payload. - 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, but no security info.
+-----+-------+-----+---------+---------+
| 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 keyed value 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 MICis the first 4 bytes of the original packet’s on-wire MIC — a publicly derivable correlation handle, not an authenticatorACK TAGis a 4-byte keyed value derived from the original packet’s MIC and the pairwise encryption key (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_enc and cannot be produced by an observer who lacks it, even one who received the original packet in its entirety. 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-route 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]| MIC |
+-----+-------+-----+-----+---------+---------+---------+------+
1 B 0/1 B 3 B 3/32B 5/7 B variable var. 4-16 B
DST is the first three bytes of the recipient’s public key.
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]| MIC |
+-----+-------+-----+------+---------+---------+---------+------+
1 B 0/1 B 3 B 3/32B 5/7 B variable var. 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_id = first_2_bytes( HKDF-SHA256(channel_key, salt="UMSH-CHAN-ID", info="", L=2) )
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) | MIC |
+-----+-------+---------+---------+---------+----------------------+------+
1 B 0/1 B 2 B 5/7 B variable 3/32 + var. 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:
+-----+-------+---------+---------+---------+------+---------+------+
| FCF |[FHOPS]| CHANNEL | SECINFO | OPTIONS | SRC | PAYLOAD | MIC |
+-----+-------+---------+---------+---------+------+---------+------+
1 B 0/1 B 2 B 5/7 B variable 3/32 B var. 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. 4-16 B
The MIC is computed over the payload using the blind unicast payload keys, which combine the pairwise shared secret with the channel key. ENC_DST_SRC is encrypted using the channel’s derived encryption key K_enc_channel (see Multicast Packet Keys) and the MIC as IV (see Security & Cryptography). Because ENC_DST_SRC decryption depends on the MIC, any tampering with the source address will produce an incorrect public key, causing pairwise key derivation to fail and payload authentication to reject.
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 | MIC |
+-----+-------+---------+---------+---------+-----+------+---------+------+
1 B 0/1 B 2 B 5/7 B variable 3 B 3/32 B var. 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
- Receiver uses
CHANNELto identify candidate channel keys. - Receiver derives the channel’s candidate keys via HKDF.
- If
E = 1, receiver reads theMICand usesK_enc_channelplusMICto decryptENC_DST_SRC, recovering the destination hint and sender address. - If
E = 0, receiver readsDSTandSRCdirectly from the cleartext packet. - Receiver converts the sender Ed25519 public key into an X25519 public key.
- Receiver converts its own Ed25519 private key into an X25519 private key.
- Receiver performs ECDH and derives the stable pairwise keys.
- Receiver computes the blind unicast payload keys by XORing the pairwise keys with the channel keys.
- Receiver authenticates the packet using the blind-unicast MIC.
- If
E = 1, receiver decryptsENC_PAYLOADusing the blind unicast payload keys. - If authentication fails, the packet is rejected.
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. 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 two least significant bits encode two semantic attributes:
- Bit 0: Critical (1) / Non-Critical (0)
- Bit 1: Dynamic (1) / Static (0)
This means a node can determine an unrecognized option’s attributes by inspecting its option number without consulting a registry.
Attribute Encoding
The four attribute combinations and their option number patterns:
| Low 2 bits | Option numbers | Classification |
|---|---|---|
0b00 | 0, 4, 8, 12, … | Non-Critical, Static |
0b01 | 1, 5, 9, 13, … | Critical, Static |
0b10 | 2, 6, 10, 14, … | Non-Critical, Dynamic |
0b11 | 3, 7, 11, 15, … | Critical, Dynamic |
Critical vs. Non-Critical
These determine behavior when a node encounters an unknown option:
- Critical (bit 0 set): if unrecognized, the packet must be dropped
- Non-Critical (bit 0 clear): if unrecognized, the option is ignored and the node continues processing
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, trace routes, station callsigns) to be modified by repeaters without invalidating end-to-end authentication.
Defined Options
| Number | Name | Classification | Value |
|---|---|---|---|
| 0 | RESERVED | Non-Critical, Static | |
| 1 | UNASSIGNED | Critical, Static | |
| 2 | Trace Route | Non-Critical, Dynamic | 0+ bytes |
| 3 | Source Route | Critical, Dynamic | 0+ bytes |
| 4 | Operator Callsign | Non-Critical, Static | ARNCE/HAM-64 |
| 5 | Minimum RSSI | Critical, Static | 0–1 bytes |
| 6 | Route Retry | Non-Critical, Dynamic | 0 bytes |
| 7 | Station Callsign | Critical, Dynamic | ARNCE/HAM-64 |
| 8 | Ack MIC | Non-Critical, Static | 4 bytes |
| 9 | Minimum SNR | Critical, Static | 0–1 bytes |
| 10 | Trace Signal | Non-Critical, Dynamic | 0–1 bytes |
| 11 | Region Code | Critical, Dynamic | 2 bytes |
Trace Route (option 2)
- Semantics: if present, repeaters prepend their own repeater hint before retransmitting.
- If absent, no trace-route information is added automatically.
- If more than one option with this number is present, the packet MUST be dropped.
- Value layout: see Trace Route Option Value.
- If implemented, Trace Signal (option 10) MUST also be implemented.
Important
If a repeater supports the Trace Route option, it MUST also implement the Trace Signal option (even if it is just adding placeholder values). Failure to do this breaks the one-to-one relationship between the Trace option and the Trace Signal options, making the Trace Signal value useless.
Source Route (option 3)
- Semantics: contains an ordered list of repeater hints designating the forwarding path.
- If more than one option with this number is present, the packet MUST be dropped.
- 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-Onlyamateur operation. - In
Hybridoperation, 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
- If more than one option with this number is present, the packet MUST be dropped.
- 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
130means-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: zero-length flag
-
Semantics: indicates that the originator is re-attempting forwarding of the same logical packet after a route it had assumed was considered failed.
-
If more than one option with this number is present, the packet MUST be dropped.
-
This option is intended for sender-originated route recovery, not for ordinary first transmission.
-
The failed assumption need not be a source route. A sender that narrowed
FHOPSbecause 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 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
- includes a trace-route 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
FHOPSto a packet that had none also sets the FCF’sHbit, and the AAD clears that bit for this reason.
Station Callsign (option 7)
- 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 critical because repeaters must replace or remove it during forwarding.
- Use:
- in
Licensed-Onlymode, repeaters replace or insert it on every forwarded packet - in
Hybridmode, repeaters also replace or insert it on every forwarded packet - in
Unlicensedmode, repeaters remove it if present and do not add their own
- in
Ack MIC (option 8)
This option represents a piggy-backed MAC ack that, when received, behaves as if it was an ack for the referenced packet, instead of sending both a MAC ack and an application-level response in two separate packets.
- 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.
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.
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) - If more than one option with this number is present, the packet MUST be dropped.
- 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.
Trace Signal (option 10)
This option works very much like the Trace Route option, except that repeaters append signal quality information instead of router hints.
When this option is present, each repeater that will repeat the packet must first prepend the signal quality metrics for the packet they received to the value of this option. The signal quality metrics are two bytes: the first byte is the negative RSSI in dBm (so -90 becomes 90, for example), and the second is the signed SNR in cB (centibells, or 1/10ths of a dB).
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:
IATA-based regions. For regions defined by proximity to an airport or a metro area with its own IATA code, encode the 3-letter IATA code into a 16-bit value using ARNCE/HAM-16. Examples:
| IATA Code | Region Code |
|---|---|
| SJC | 0x7853 |
| MFR | 0x5242 |
Named regions. For regions that are not associated with a single airport (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 region name (UTF-8 encoded), EXCEPT when performing ARNCE/HAM-16 decoding on the resulting value would yield three letters. In that case, you additionally perform the following transform:
def transform_letter_chunk(encoded: int) -> int:
"""Transform a three-letter ARNCE chunk into a non-letter ARNCE chunk."""
LETTER_MIN = 1
LETTER_MAX = 26
TRANSFORM_BASE = 27 * 1600 # 0xA8C0
TRANSFORM_COUNT = 26 ** 3 # 17,576
a = encoded // 1600
b = (encoded // 40) % 40
c = encoded % 40
if not all(LETTER_MIN <= x <= LETTER_MAX for x in (a, b, c)):
return encoded
rank = (a - 1) * 26 * 26 + (b - 1) * 26 + (c - 1)
return TRANSFORM_BASE + rank
Examples:
| Region Name | SHA-256 prefix | Region Code |
|---|---|---|
| Rogue Valley | 0xdf6f... | 0xdf6f |
| SF Bay Area | 0x31d9... | 0x31d9 |
| Southern Oregon | 0x6af2... | 0xD35F |
Note that the first two bytes of the SHA256 of “Southern Oregon” is 0x6AF2, which would decode to QDR, so it is transformed to 0xD35F (which would decode as 654).
Thus, non-IATA-based region codes will never collide with IATA-based region codes. This allows all region codes which decode to three letters to be assumed to be an IATA region code and can be used/displayed unambiguously without additional context.
However, collisions can still happen between hash-originated region codes. 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 non-IATA-based region codes—and resolution of any collisions—are generally handled locally.
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 Route Option Value
A trace-route option also contains zero or more router hints:
+----------+----------+----------+-----+
| RH[0] | RH[1] | RH[2] | ... |
+----------+----------+----------+-----+
2 B 2 B 2 B
Repeaters prepend their 2-byte router hint:
new_trace = my_router_hint || old_trace
So the list is ordered most-recent repeater first.
Security & Cryptography
UMSH authenticates and optionally encrypts packets using a construction inspired by AES-SIV (RFC 5297). 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.
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 | RESERVED |
+---+-------+---+---------------+
1b 2 bits 1b 4 bits
Where:
E= encrypted payload flagMIC= MIC size codeS= salt includedRESERVED= 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:
| Value | MIC Length |
|---|---|
| 0 | 4 bytes |
| 1 | 8 bytes |
| 2 | 12 bytes |
| 3 | 16 bytes |
The MIC is produced by computing the full 16-byte AES-CMAC and truncating to the specified length. 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 for a given shared secret and traffic direction. UMSH uses this monotonic counter — rather than timestamps — for replay protection, keeping the protocol free of any dependency on synchronized clocks or absolute time.
The exact mechanism for how the frame counter is handled is implementation specific, assuming that it always increases. For example, the frame counter may be unique for each source+destination node pair, or it may be a single frame counter for the entire device. On constrained devices, it may make sense to use a combination of the two: have a fixed set of counters (say, 32) that are initialized with random starting values, and derive a pseudo-random number from 0-31 from the shared secret to pick which of those counters is being used.
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 packet MICs (similar to the approach used for duplicate suppression in repeaters): if the MIC is already present, the packet is a replay and is rejected; if not, the packet is accepted and its MIC is 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.
Counter Persistence
How a node persists and recovers its 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.
Caution
If the counter is 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. If a known peer’s frame counter subsequently falls outside the forward window — for example, after the peer reboots and loses its persisted counter — the receiver MAY use the Echo Request MAC command (including a nonce, see MAC Commands) to determine the peer’s current counter value and re-establish a valid baseline.
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:
- Start with sender Ed25519 keypair and recipient Ed25519 keypair.
- Convert both Ed25519 keys to X25519 form.
- Perform X25519 ECDH.
- Feed the resulting shared secret into HKDF-SHA256.
- 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 should reject malformed public keys before conversion.
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 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-V1"
okm = HKDF-SHA256(ikm, salt, info, 32)
The output keying material is split as follows:
K_enc = okm[0..15]
K_mic = okm[16..31]
Where:
K_encis the 16-byte encryption keyK_micis the 16-byte message authentication key
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 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_pairwiseare the stable pairwise keys derived from the sender/recipient ECDH shared secretK_enc_channel,K_mic_channelare 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 16-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 using AES-SIV:
K_encandK_micare the stable pairwise keysSECINFOand 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_micis 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 should use it for replay detection even though AES-SIV is resistant to nonce misuse.
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-V1" || channel_id
okm = HKDF-SHA256(ikm, salt, info, 32)
K_enc = okm[0..15]
K_mic = okm[16..31]
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 a construction inspired by AES-SIV (RFC 5297), with the MIC and encryption steps separated to allow future support for different MIC lengths.
The processing is:
- Compute the full 16-byte AES-CMAC over the AAD and plaintext using
K_mic. - Truncate the CMAC to the MIC length specified by the SCF.
- Construct the CTR IV from the MIC (see CTR IV Construction).
- Encrypt the plaintext using AES-128-CTR with
K_encand the constructed IV.
The MIC is transmitted separately from the ciphertext (not prepended as in standard AES-SIV), allowing the MIC length to be controlled independently via the SCF MIC size field.
A key design goal is robustness against nonce reuse. Because the CTR IV is derived from the MIC (as in SIV-style constructions), accidental reuse of nonces or counters is not cryptographically catastrophic in the way it would be for CTR or GCM.
CTR IV Construction
The 16-byte CTR IV is constructed by appending the SECINFO field to the MIC, then zero-padding or truncating the result to exactly 16 bytes:
IV = truncate_or_pad_to_16( MIC || SECINFO )
For the 16-byte MIC, SECINFO is entirely truncated away and the IV equals the MIC — identical to standard AES-SIV. For shorter MICs, the IV incorporates the frame counter and optional salt from SECINFO, providing additional per-packet IV variability that compensates for the increased probability of truncated-MIC collisions.
| MIC Length | SECINFO (5 B) | SECINFO (7 B) | SECINFO bytes in IV |
|---|---|---|---|
| 16 B | truncate to 16 | truncate to 16 | 0 (IV = MIC) |
| 12 B | truncate to 16 | truncate to 16 | 4 |
| 8 B | zero-pad to 16 | zero-pad to 16 | 5 or 7 |
| 4 B | zero-pad to 16 | zero-pad to 16 | 5 or 7 |
Unencrypted Packets
When encryption is disabled, the MIC is calculated using CMAC with K_mic.
Associated Data
The associated data (AAD) binds the immutable header fields to the MIC so that any modification is detected.
The AAD is constructed by concatenating the following fields in order:
- FCF (1 byte, with the flood-hops-present (
H) bit cleared) - Static options — re-encoded as type-length-value (see below)
- DST (3-byte destination hint, unicast) or CHANNEL (2 bytes, multicast)
- SRC (3-byte hint or 32-byte full key) — included only when the source field is outside the ciphertext
- SECINFO (5 or 7 bytes)
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 DST/CHANNEL, SRC, and SECINFO enter the AAD as different values and the MIC check still fails.
Note that the AAD ordering above is canonical and differs from wire ordering. On the wire, static options appear after SECINFO (immediately before the payload); in the AAD they appear at position 2, before DST/CHANNEL. Wire ordering and AAD ordering are intentionally decoupled so that the canonical AAD structure remains stable regardless of where options sit on the wire.
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.
Ack Tag Construction
When a packet type requests an acknowledgement (UNAR or BUAR), the acknowledgement carries two fields that are computed independently by both the sender and the receiver: 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 a keyed value that only the original sender and the final destination can produce. It is computed as follows:
- Compute the full 16-byte AES-CMAC over the AAD and plaintext using
K_mic(this is the same computation used to produce the packet MIC, before any truncation). - Encrypt the 16-byte CMAC with a single AES-128-ECB block encryption using the pairwise
K_enc. - Truncate the result to 4 bytes.
ack_tag = truncate_to_4( AES-128-ECB( key=K_enc, block=full_16B_CMAC ) )
Where:
K_encis the encryption key used for the 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)full_16B_CMACis the full 16-byte AES-CMAC computed during packet processing, before truncation to the on-wire MIC length
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. The keyed ack_tag, by contrast, never appears in the original packet: producing it requires knowledge of K_enc, so a passive observer who intercepts the original packet cannot forge a valid standalone ack even though ack_mic is public. With a 4-byte keyed tag, a blind forgery succeeds with probability 2^-32 per attempt; over a bandwidth-limited LoRa channel, online guessing at that scale is infeasible. A successful forgery would cause the sender to treat an undelivered packet as delivered and suppress retransmission — a reliability denial-of-service, not a confidentiality or integrity 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.
AES-ECB on a single 16-byte block is the raw AES block cipher — a pseudorandom permutation — and does not have the pattern-leakage weakness associated with multi-block ECB encryption.
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-128-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 HKDFIV= CTR IV constructed from the packet MIC and SECINFODST= 3-byte destination hintSRC= source address: 3-byte source hint whenS=0, or 32-byte source public key whenS=1
Then:
ENC_DST_SRC = AES-128-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).
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:
-
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.
-
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-route option: when a flooded packet carries a trace-route option, each forwarding repeater prepends its own hint. The recipient can reverse the accumulated trace and cache it as a source route for future replies. This means path discovery is not a separate operation — it falls out of normal packet exchange.
See Packet Options § Source Route, Packet Options § Trace Route, 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: they consume source-route hints and forward packets as repeaters do. Currently, a bridge also retransmits on the inbound medium — even for source-routed packets — to provide forwarding confirmation to the previous hop, though this may be optimized in the future.
Bridges participate in source routes and trace routes like any other repeater. A trace route that crosses a bridge will contain the bridge’s router hint, and source-routed packets will traverse the bridge transparently.
Flooding works across bridges, but the remaining flood hop count is clamped when a packet exits the bridge — by default, to a maximum of 1. This clamping applies even to hybrid-routed packets that transition from source routing to flooding after crossing the bridge. The effect is to keep individual meshes local and accountable while still enabling multi-segment routing.
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 the flood hop count of packets which transit the bridge.
A client–server tunnel realization of an internet bridge — including the exact form of the inbound-medium retransmission — is specified in Internet Bridging.
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.
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, so that better-positioned repeaters transmit first and weaker ones 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.
Receive Procedure
-
Well-formedness check
- If the packet is malformed (invalid FCF, truncated fields, non-zero reserved bits in the SCF), drop.
-
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.
- If the packet is a MAC Ack:
-
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).
-
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.
- Decrypt the source address using the channel’s derived encryption key
-
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.
-
Payload type validation
- If the payload type is not allowed for this packet type (see Payload and Packet Type Compatibility), drop.
-
Replay and Ack Processing
- 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 keyed value from the full 16-byte CMAC and pairwise
K_enc— 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.
- Otherwise, if replay detection rejects the packet, drop it without acknowledging it.
-
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 Source 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 a 2-byte channel identifier derived from the channel key. This identifier is a compact hint that allows receivers to quickly identify candidate channels without attempting decryption with every configured key. Like destination hints, channel identifiers are not cryptographically authoritative — collisions are possible and must be resolved by attempting cryptographic verification.
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.
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:
- Percent-decode the name portion of the URI.
- 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.
- Fold ASCII letters
A–Zto lowercasea–z. No other characters are altered. - 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: 5 without a region code, 7 with a region code.
- 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: 5 without a region code, 7 with a region code.
- 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). - Chat messages must include an EdDSA signature in the payload.
- Messages that do not meet all three requirements (unencrypted, full source key, signed) must not be accepted or displayed by the user interface.
These requirements ensure that emergency traffic is universally readable, attributable to a specific node, and cryptographically authenticated against impersonation.
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.
For reference, typical T_frame values for a maximum-length (255-byte) packet using common MeshCore-style channel settings:
| Region | Settings | T_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:
- Wait a random duration uniformly sampled from [0, T_frame].
- Perform CAD again.
- Repeat up to 4 more times (5 CAD attempts total).
- 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 inversely proportional to the quality of the received signal. Nodes that heard the packet most clearly transmit first; nodes that barely met the signal threshold wait longer. 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:
quality = clamp((received_SNR − SNR_low) / (SNR_high − SNR_low), 0, 1)
W = W_min + (W_max − W_min) × (1 − quality)
delay = D_ack + uniform_random(0, W)
Where:
SNR_lowandSNR_highdefine the clamp range used for the contention heuristic. The suggested defaults are −6 dB and +15 dB, respectively.- 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.)
W_minis the minimum contention window for strong receptions. The suggested default is 0.2 × T_frame.W_maxis the maximum intentional forwarding-delay window. The suggested default is 2 × T_frame.received_SNRis the SNR measured during reception of the packet being forwarded.D_ackis the ACK protection interval: a guard delay that applies when the forwarded packet may elicit an immediate ACK from its destination, and zero otherwise.
If SNR is unavailable but RSSI is, the same formula MAY be applied with RSSI values substituted for SNR, using appropriate threshold and range parameters.
After computing the delay, the repeater waits. Other packets SHOULD continue to be 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 resample a 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. A repeater SHOULD NOT do this more than 3 times; after the third such deferral it SHOULD abandon the pending forward.
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_ackof ACK protection delay, when it applies - up to
W_maxof intentional forwarding delay - up to
T_framefor 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 sampled contention delay uniform_random(0, W) 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 sampled 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 hint to the trace route 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 route, hop count, etc)
- Typical examples are flood hop counts, trace routes, 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.
- 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
- 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.
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
-
Duplicate suppression
- If this packet was forwarded recently, do not forward.
-
Local origin and local destination
- 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.
-
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.
-
Unknown critical options
- If the packet contains any critical option the repeater does not understand, do not forward.
-
Policy checks
- If the packet does not satisfy local repeater policy, do not forward.
-
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 route 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.
- If the packet contains a non-empty source-route option:
-
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.
-
Flood hop accounting (flood forwarding only!)
- If the packet has a flood hop count field with
FHOPS_REM > 0, decrementFHOPS_REMand incrementFHOPS_ACC. - Otherwise, do not forward.
- If the packet has a flood hop count field with
-
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.
-
Trace route processing
- If the packet contains a trace-route option, prepend this repeater’s hint. If prepending the hint would cause the packet to exceed the maximum frame size, drop the packet.
- Retransmit
- If this is a flood forward, implement the flood forwarding contention window
- 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.
Bridges follow the same packet-rewrite rules as repeaters.
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.
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 + D_ack
where W_max is the maximum intentional forwarding-delay window permitted for the path and D_ack is the ACK protection interval when it applies. With the suggested defaults W_max = 2 × T_frame and D_ack = 0.25 × T_frame, this yields confirm_timeout = 4.25 × T_frame.
If the packet is heard before confirm_timeout expires, forwarding is confirmed.
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.
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 cannot verify the keyed 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
FHOPSand leaves no trace in the options
A practical recovery rule is:
- if the sender exhausts the retry budget for a packet sent against a cached route, it SHOULD treat that cached route as failed
- the failed route SHOULD be discarded or marked unusable for immediate reuse
- 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-route 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.
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:
- resample a forwarding delay using the contention-window procedure in Channel Access
- restart the waiting period
- after 3 such deferrals, 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-route 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.
Pairing the trace-route option with Trace Signal makes the beacon report 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. 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 a signed identity 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 neighbours 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:
-
Outbound discovery: Node A sends a unicast packet to Node B with the trace-route option present and an appropriate flood hop count. The packet floods through the mesh; repeaters prepend their router hints to the trace-route option as they forward.
-
Path learning: When Node B receives the packet, the trace-route option contains the sequence of repeater hints traversed, ordered most-recent first. Node B can use this list directly as a candidate source route back to Node A.
-
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.
-
Bidirectional establishment: If Node A also needs a source route to Node B, it can include the trace-route option on its initial packet. When Node B responds (e.g., with an ack, beacon, or identity payload) using its learned route and also including a trace-route option, Node A can learn its own source route to Node B.
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
When a node successfully processes an incoming packet, it SHOULD update its routing state for the sender:
- Trace route: if the packet contains a trace-route option, the node caches that trace route as a source route for future packets back to the sender. Because the trace route 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.
- Flood hop count: if the packet contains a flood hop count, the node caches the sender’s
FHOPS_ACCvalue 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.
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 route, and SHOULD discard cached routes that have proven unreachable.
In practice, “proven unreachable” usually means that an ack-requested packet sent using the cached source route exhausted its retry budget without end-to-end success. In that case, the sender should stop trusting the stale route and return to route-discovery behavior:
- 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-route 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.
Once the peer replies and a fresher trace route is learned, the sender can resume normal source-routed transmission using the replacement route.
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 small margin:
- Source route: the route constrains every hop until it empties, and only the final repeater spends flood budget, so one hop covers the route itself.
- Flood distance: the cached
FHOPS_ACCis the radius at which the destination was last heard. - Direct link: no forwarding hop is needed at all.
The margin — one hop is a reasonable default — keeps delivery self-healing when the path has grown by a hop since it was learned, without paying for a mesh-wide flood on every packet. 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.
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-route option
- allow only a small flood budget, capped at no more than
source_route_hops + 1 - perform this no more than occasionally, for example no more than once every
Nsuccessful transmissions and no more than once everyTminutes, 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.
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
| Value | Meaning |
|---|---|
| 0 | Unspecified |
| 1 | Node Identity |
| 2 | MAC Command |
| 3 | Text Message |
| 4 | RESERVED |
| 5 | Chat-Room Message |
| 6 | RESERVED |
| 7 | CoAP-over-UMSH |
| 8 | Node Management Command |
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 Type | Unicast | Multicast | Broadcast |
|---|---|---|---|
| Empty payload | Yes | Yes | Yes |
| Node Identity | Yes | Yes | Yes |
| MAC Command | Yes | Note 1 | Note 2 |
| Text Message | Yes | Yes | No |
| Chat-Room Message | Yes | No | No |
| CoAP-over-UMSH | Yes | Yes | No |
| Node Management Cmd | Yes | No | No |
Unless explicitly configured otherwise, the only payload types allowed for broadcast are empty payloads, node identities, and the broadcast-permitted MAC commands (Note 2).
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.
In-Band Node Management
Nodes may optionally support remote management via Node Management Command 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 coveringROLEthrough the0xFFterminator, inclusive.
The smallest node identity payload is two bytes: role and capability bitmap, with no options or signature.
Node Primary Role
Defined values:
0— Unspecified1— Repeater2— Chat3— Tracker4— Sensor5— Bridge6— Chat Room7— 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.
| Number | Name | Value |
|---|---|---|
| 0 | Node Name | UTF-8 string |
| 1 | Node Location | 1-7 bytes, see Variable-Precision Location Format |
| 2 | Altitude in Meters | signed integer, meters above mean sea level |
| 3 | Unix Timestamp | unsigned integer, seconds since the Unix epoch, UTC |
| 4 | Supported Flood Regions | one or more concatenated 2-byte region codes |
| 5 | Nonce | 4 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 list of region codes the node will flood-forward for. Entries are 2 bytes each, concatenated with no delimiter. A node that omits this option makes no claim about its regional forwarding policy. Max length: 20 bytes.
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 = 0xB51lon_index = floor((−121.883 + 180) × 4096 / 360) = floor(661.24) = 661 = 0x295
Reading nibbles most-significant first:
| Byte | High (lat) | Low (lon) | Value |
|---|---|---|---|
| 0 | 0xB | 0x2 | 0xB2 |
| 1 | 0x5 | 0x9 | 0x59 |
| 2 | 0x1 | 0x5 | 0x15 |
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:
| Bytes | Latitude cell | Longitude cell | Equator cell size (approx.) |
|---|---|---|---|
| 1 | 11.25° | 22.5° | 1,250 × 2,500 km |
| 2 | 0.703125° | 1.40625° | 78 × 156 km |
| 3 | 0.0439° | 0.0879° | 4.9 × 9.8 km |
| 4 | 0.00275° | 0.00549° | 305 × 610 m |
| 5 | 0.000172° | 0.000343° | 19 × 38 m |
| 6 | 1.07 × 10⁻⁵° | 2.15 × 10⁻⁵° | 1.2 × 2.4 m |
| 7 | 6.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.
Recommended Construction
- 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.
- Uniform distribution. Draw the offset uniformly, over either a disk of radius
R(isotropic) or a box of half-widthR(simplest: two independent uniform components taken directly from a keyed hash). A uniform offset makes every position withinRof 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 withinRof the report — but every bounded distribution shares this, and unbounded tails trade it for occasionally reporting positions an absurd distance away. - Magnitude matched to the published precision. Choose
Rbetween 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, whileRtunes ambiguity continuously between those steps. - 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. - 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.
- 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 is generally included only when the identity data must stand on its own without any authentication, such as:
- QR codes
- broadcasts
When the enclosing packet already carries a MIC, the EdDSA signature MAY be omitted.
A signature is checkable only against the sending node’s public key, so a signed broadcast advertisement MUST carry its source address in full-key form. A hint-only advertisement can be verified only by a receiver that already holds the key, which is not the audience an advertisement is for.
MAC Commands
A MAC command payload consists of:
- 1 byte: command identifier
- optional bytes: command-specific payload
Support for MAC commands is optional.
Command Registry
| Value | Command | Direction |
|---|---|---|
| 0 | UNALLOCATED | – |
| 1 | Identity Request | Request |
| 2 | Signal Report Request | Request |
| 3 | Signal Report Response | Response |
| 4 | Echo Request | Request |
| 5 | Echo Response | Response |
| 6 | PFS Session Request | Request |
| 7 | PFS Session Response | Response |
| 8 | End PFS Session | Either |
| 9 | No-op | Request |
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 FILTER_NODE_HINT filter names a single node, so a request carrying one solicits a single reply however far it travels. A request without one selects by role or capability, and every node it reaches may answer; such a request is therefore confined to the requester’s own neighbourhood:
- 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), keeping the reply within the single hop the request was allowed to cross.
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.
| Key | Critical | Name | Value | Description |
|---|---|---|---|---|
| 1 | Yes | NONCE | 4 bytes | Correlation identifier the responder MUST echo in its response’s Nonce option. Not a filter. |
| 3 | Yes | FILTER_NODE_HINT | 3 bytes | Respond only if this matches the responder’s own node hint. |
| 5 | Yes | FILTER_NODE_ROLE | 1 byte | Respond only if the responder’s primary role equals this value. |
| 7 | Yes | FILTER_NODE_CAPS | 1 byte | Respond 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.
| Field | Size | Description |
|---|---|---|
| RSSI | 1 byte | Received signal strength as an unsigned value representing negative dBm (e.g. 130 = -130 dBm) |
| SNR | 1 byte | Signal-to-noise ratio as a signed value in dB |
Echo Request (4)
Requests that the destination respond with an Echo Response.
| Field | Size | Description |
|---|---|---|
| Echo data | 0+ bytes | Arbitrary 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.
| Field | Size | Description |
|---|---|---|
| Echo data | 0+ bytes | Copied verbatim from the Echo Request |
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.
| Field | Size | Description |
|---|---|---|
| Ephemeral node address | 32 bytes | Sender’s newly generated ephemeral node address (Ed25519 public key) for this session |
| Session duration | 2 bytes | Requested session lifetime in minutes (0 = no expiration) |
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.
| Field | Size | Description |
|---|---|---|
| Ephemeral node address | 32 bytes | Responder’s newly generated ephemeral node address (Ed25519 public key) for this session |
| Session duration | 2 bytes | Accepted session lifetime in minutes |
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.
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. A Node
Management Command payload (payload type 8) carries 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 is answered again, not executed again;
- batches order execution within a single payload, for writes whose effects depend on sequence;
- cursors carry values larger than one frame across as many exchanges as needed, without per-read state on the device.
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
A Node Management Command payload consists of, following the payload type byte:
+-------+-------+---------+------+------------+
| FLAGS | TOKEN | OPTIONS | 0xFF | FRAME LIST |
+-------+-------+---------+------+------------+
1 B 2 B variable 1 B variable
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 as ordinary unicast replies, using whatever routing state the exchange has supplied (see Route Learning). A device drops a Node Management payload arriving by multicast or broadcast, with accounting.
Flags
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| R | RESERVED |
+---+---+---+---+---+---+---+---+
Bit 7 (R) is clear in a request and set in a response. A device drops a
payload with R set — it never solicits anything — and a node that
receives a response matching no outstanding exchange of its own discards
it.
The reserved bits MUST be zero. A receiver drops a payload with any reserved bit set, with accounting: an unknown flag may change the meaning of everything that follows, including the token, so no response can be formed.
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.
| Number | Critical | Name | Value |
|---|---|---|---|
| 1 | Yes | CURSOR | 1–8 octets, see Reading Large Values |
| 2 | No | REMAINING | PUI, see Reading Large Values |
The 0xFF end-of-options marker is always present, since the frame list
follows.
Frame List
One or more ULCP frames, each preceded by its length in octets encoded as a packed unsigned integer:
+--------------+----------------------------+
| LENGTH (PUI) | ULCP FRAME (LENGTH octets) |
+--------------+----------------------------+
Embedded frames use the exact frame format of the local bindings, so a device dispatches them through the same machinery that serves its local link. Senders MUST set the TID bits of every embedded frame to zero, and receivers ignore them: correlation is by token, and within a payload by position. A payload containing no frames is dropped, with accounting.
The payload, envelope included, must fit a single UMSH frame; there is no fragmentation. The reserved flag bits and the unassigned option numbers are this payload’s growth space: a future need — carrying a request larger than one frame, say — is met by assigning one of them, 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, sets R, and contains one frame per
executed request frame, in request order: exactly the frame the device
would emit in reply on a local binding — a CMD_PROP_IS,
CMD_PROP_INSERTED, or CMD_PROP_REMOVED 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.
Batches
A payload may carry several frames; the device executes them strictly in order, so a batch is how an administrator expresses writes whose effects depend on sequence. A batch is a sequencing construct, not a transaction: Mutation Atomicity applies to each frame alone, and an interrupted batch leaves the earlier frames applied.
The device stops executing a batch at:
- the first frame whose response is a
PROP_LAST_STATUSframe reporting an error — any status other thanSTATUS_OK; - the first frame whose response would not fit the remaining space in the response payload;
- any frame that initiates a reset;
- a frame that cannot be parsed, whose response frame is
STATUS_PARSE_ERROR.
Request frames past the stopping point are not executed and produce no response frames. An administrator that receives fewer response frames than it sent request frames examines the last response frame it did receive: an error status means the batch stopped on that failure; a success means it stopped for space, and the administrator reissues the remainder as a new exchange. A value too large to fit whole within the remaining space stops the batch the same way; the administrator reads that property alone, where fragmentation applies (see Reading Large Values).
Commands that initiate a reset — CMD_RST, CMD_RESTORE in its reset
form, and CMD_FACTORY_RESET — produce no response frame and
terminate the batch. 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.
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. The device therefore 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 device MAY bound how many administrators it retains an entry for, evicting the least recently active, but retains at least the entry for the most recently active administrator.
An administrator that receives no response retransmits the identical request with the identical token, paced to the path’s round-trip behavior; the retained response makes this safe whether the request or only its response was lost. 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 property value that does not fit one response is read across several
exchanges. The device returns a leading fragment of the value together
with a CURSOR option: an opaque continuation handle, one to eight
octets, chosen entirely by the device. The administrator continues with a
new exchange — fresh token — whose request carries the returned cursor
verbatim alongside the same CMD_PROP_GET. 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.
A request carrying a CURSOR option MUST consist of exactly one frame, a
CMD_PROP_GET; anything else is answered STATUS_INVALID_ARGUMENT. The
device fragments only when answering such a single-GET payload — within
a larger batch, a value too large for the remaining response budget stops
the batch instead (see Batches).
The contract:
- A cursor is meaningful only to the device that issued it, and only for the property it was issued for. The administrator returns it byte-for-byte and MUST NOT construct or modify one.
- For a multi-value property, fragment boundaries MUST fall on item boundaries, so that every fragment is a well-formed item sequence on its own, the property’s item length prefix rule applying within each fragment. A single-value property’s value is split at arbitrary octet boundaries and reassembled by concatenation.
- Presenting the same cursor again SHOULD yield the same fragment or an equivalent one; 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, or the
underlying data has changed out from under the position — with
STATUS_CURSOR_INVALID(see Status Codes); the administrator restarts from a cursor-less request. A practical cursor encodes the position together with a generation of the underlying data — a table revision, a boot count — so that every change that invalidates positions is detected rather than served wrong. - A response MAY carry an empty fragment with a cursor equal to the one presented, meaning nothing further is available yet; this suits data that accumulates over time.
- A response MAY carry a REMAINING option: the approximate number of items not yet returned, as a packed unsigned integer. It is advisory, for progress reporting.
- The read holds no state on the device: between exchanges, the position lives entirely in the cursor the administrator holds.
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 reach the device domain and the device-scoped protocol
state — including PROP_CAPS, so capability discovery works exactly as on
the local link. Out of reach are:
- session state and the host domain
(see State Classes): a request naming such
a property is answered as an unrecognized property,
STATUS_PROP_NOT_FOUND; CMD_STR_SENDandCMD_QUEUE_DRAIN, answered as unrecognized commands,STATUS_INVALID_COMMAND;PROP_DEV_PRIVATE_KEY, answeredSTATUS_PROP_NOT_FOUND: 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
| Code | Name | Requires | Grants |
|---|---|---|---|
| 43 | CAP_ADMIN | CAP_DEV_IDENTITY | Node management: processing of Node Management Command 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.
| Number | Name | Value |
|---|---|---|
| 0 | Message Type | 0 or 1 bytes |
| 1 | Sender Handle | UTF-8 string |
| 2 | Message Sequence | 1 or 3 bytes (see below) |
| 3 | Sequence Reset | 0 bytes (flag) |
| 4 | Regarding | 1 or 4 bytes (see below) |
| 5 | Editing | 1 byte |
| 6 | Background Color | 3 bytes, RGB |
| 7 | Text Color | 3 bytes, RGB |
| 8 | Channel Group Resend | 0 bytes (flag) |
| 9 | Padding | Anything |
Message Type
| Value | Name | Rendering |
|---|---|---|
| 0 | Basic text | Displayed in a text bubble |
| 1 | Status text | Displayed inline as “[HANDLE] [MESSAGE]” (similar to IRC /me) |
| 2 | Message Resend Request | Not displayed; see the body rules below |
| 3 | Message Unavailable | Not 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 one when a body carries more.
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):
| Byte | Field | Description |
|---|---|---|
| 0 | Message ID | Per-sender message identifier (wraps at 255) |
3-byte form (fragment):
| Byte | Field | Description |
|---|---|---|
| 0 | Message ID | Shared by all fragments of the same message |
| 1 | Fragment Index | Zero-based position of this fragment |
| 2 | Fragment Count | Total 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, authentication proves only possession of the channel key; the source hint is a claimed identity, and the protocol cannot prevent one channel member from impersonating another member.
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.
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.
| Value | Action | Direction |
|---|---|---|
| 0 | Get Room Info | User → Room |
| 1 | Room Info | Room → User |
| 2 | Login | User → Room |
| 3 | Logout | User → Room |
| 5 | Fetch Messages | User → Room |
| 6 | Fetch Users | User → Room |
| 7 | Admin Commands | User → Room |
| 8 | Room Update | Room → 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:
| Number | Name | Notes |
|---|---|---|
| 0 | Room Name | UTF-8 string |
| 1 | Owner Information | |
| 2 | Administrator | User ID; may appear more than once. May only be included for logged-in users |
| 3 | Active User Count | |
| 4 | Max User Count | |
| 5 | Message Queue Depth | |
| 6 | Most Recent Message Timestamp | |
| 7 | Oldest 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:
| Number | Name | Notes |
|---|---|---|
| 0 | Handle | If omitted, the room uses the previous handle or assigns one |
| 1 | Last Message Timestamp | If present, the room sends up to 10 missed messages since this time |
| 2 | Session Timeout | Requested inactivity timeout in minutes (1 byte) |
| 3 | Password | Required 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.
| Field | Size | Description |
|---|---|---|
| Timestamp | 4 bytes | Fetch messages up to and including this time |
| Max Count | 1 byte | Maximum 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.
| Number | Name | Value |
|---|---|---|
| 12 | Timestamp Received | 4 bytes, UTC UNIX timestamp |
| 13 | Sender Sequence | 1 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:
| Value | Name |
|---|---|
| 32 | User Joined |
| 33 | User Left |
| 34 | Admin 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.
| Value | Action | Direction |
|---|---|---|
| 8 | Room Update | Room → 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 namemh= recommended maximum flood hopsr= 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:
- Framing and Common Semantics defines the frame format, the command grammar and property model, the classes of state a device holds, and the status, reset, and capability registries
- one chapter per subsystem — Radio Control, Frame Transport, Device Domain, Saved State, and Tethered Host Services — each defining its own capabilities, commands, and properties
- Minimum Requirements states what a device has to implement to be a ULCP device, and the Command and Property Index locates every numeric identifier
- ULCP over BLE binds the protocol to BLE GATT; serial transports (UART, USB-CDC) use HDLC-Lite framing
- Node Management carries the same grammar over the mesh itself, letting an authorized administrator reach a device’s device domain in-band, with no local link at all
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
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 five 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.
Suggested Capability Matrix
The table below summarizes which side owns which function.
| Capability | Host | Device |
|---|---|---|
| Long-term private identity key | Yes | No |
| Fresh pairwise derivation for arbitrary new peers | Yes | No |
| Raw frame transmit / receive | Optional | Yes |
| Radio parameter control | Configure | Enforce |
| Receive filtering | Configure | Enforce |
| Channel keys | Yes | Optional, provisioned |
| Pairwise keys for known peers | Yes | Optional, provisioned |
| MAC acks for provisioned peers while host disconnected | Configure/policy | Perform |
| Inbound queueing while host absent | Drain/consume | Perform |
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
- 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; 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 mode | Typical role for UMSH | Payload-size implications |
|---|---|---|
| GATT | Tethered host-to-device ULCP link | Large enough for full ULCP frames and often full UMSH frames without special contortions |
| L2CAP CoC | Tethered host-to-device ULCP link where available | Similar role to GATT, often a cleaner framing substrate |
| LE advertising / scan response | Discovery, announcements, tiny local messages | Small; should not be treated as a full-frame bearer without fragmentation |
| Periodic advertising | Scheduled broadcast / downlink-style announcements | Still advertising-scale payloads; better for scheduled broadcast than general frame transport |
| PAwR | Scheduled low-rate local access with responses | More interesting for shared local access, but still a constrained bearer compared with GATT |
| Bluetooth Mesh bearers | Separate larger design space | Potentially 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.
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:
- 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.
- Order the bytes from least-significant to most-significant. (Little-endian)
- 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, and the four state-management
commands in Saved State. The complete numeric
allocation is in the Command and Property Index.
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 0 | CMD_NOP | Host->Device | No-Operation |
| 1 | CMD_RST | Host->Device | Reset the device |
| 2 | CMD_PROP_GET | Host->Device | Get property value |
| 3 | CMD_PROP_SET | Host->Device | Set property value |
| 4 | CMD_PROP_INSERT | Host->Device | Insert an item into a multi-value property |
| 5 | CMD_PROP_REMOVE | Host->Device | Remove an item from a multi-value property |
| 6 | CMD_PROP_IS | Device->Host | Property value notification |
| 7 | CMD_PROP_INSERTED | Device->Host | Item-inserted notification |
| 8 | CMD_PROP_REMOVED | Device->Host | Item-removed notification |
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.
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
Ismeans that the property may be emitted asynchronously. All properties that supportGetorSetwill emit anIsto respond with the current/new value of that property.
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 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 withCMD_PROP_ISwhose 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 aCMD_PROP_IScarrying 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_SETwhose 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 settingPROP_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, orCMD_PROP_REMOVEDnotifications 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.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 0 | PROP_LAST_STATUS | Get, Is | Last status |
| 1 | PROP_PROTOCOL_VERSION | Get | Protocol version |
| 2 | PROP_DEV_VERSION | Get | Device version string |
| 3 | PROP_INTERFACE_TYPE | Get | Interface type |
| 5 | PROP_CAPS | Get | Capabilities |
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.
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 session-scoped properties —
currently only PROP_MAC_PROMISCUOUS. 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_*), includingPROP_PHY_ENABLED, and the duty-cycle limit - the human-readable device name (
PROP_DEV_NAME) - live battery telemetry (
PROP_BATTERY), whenCAP_BATTERYis 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_KEYitself- 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:
| Range | Class |
|---|---|
| 0–31 | Core protocol state |
| 32–47 | Radio control (PROP_PHY_*) |
| 48–63 | Session-scoped and global protocol state |
| 64–95 | Device domain |
| 96–111 | Host domain (PROP_HOST_*) |
| 112–127 | Streams (STR_*) |
| 4608–4863 | Extended radio control |
| 4864–5119 | Extended 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.
- 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, 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:
CMD_PROP_GETforPROP_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.CMD_PROP_GETforPROP_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.CMD_PROP_GETfor the device-domain properties the host depends on (PROP_SAVED, thePROP_PHY_*configuration), and forPROP_HOST_RX_QUEUE_COUNT.- 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. - Issue
CMD_QUEUE_DRAINwhen 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.
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_GETand all device-emitted notifications report key-bearing properties without their secrets (see Multi-Value Properties): peer public keys withoutK_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_SETandCMD_PROP_INSERTfor the key tables, and any set ofPROP_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_SAVEMUST 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.
| Id | Name |
|---|---|
| 0 | STATUS_OK |
| 1 | STATUS_FAILURE |
| 2 | STATUS_UNIMPLEMENTED |
| 3 | STATUS_INVALID_ARGUMENT |
| 4 | STATUS_INVALID_STATE |
| 5 | STATUS_INVALID_COMMAND |
| 7 | STATUS_INTERNAL_ERROR |
| 9 | STATUS_PARSE_ERROR |
| 10 | STATUS_IN_PROGRESS |
| 11 | STATUS_NOMEM |
| 12 | STATUS_BUSY |
| 13 | STATUS_PROP_NOT_FOUND |
| 18 | STATUS_CCA_FAILURE |
| 19 | STATUS_ALREADY |
| 20 | STATUS_ITEM_NOT_FOUND |
| 21 | STATUS_CURSOR_INVALID |
| 32 | STATUS_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_STATEin 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_INSERTis already present in the property. STATUS_ITEM_NOT_FOUND- The item or selector passed to
CMD_PROP_REMOVEdoes not match any item in the property. STATUS_CURSOR_INVALID- The cursor presented in a Node Management continuation is not one the device can honor — it does not parse, names a different property, or the underlying data has changed so that the position is meaningless. The administrator restarts the read from an initial, cursor-less request.
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.
| Id | Name |
|---|---|
| 112 | STATUS_RESET_POWER_ON |
| 113 | STATUS_RESET_EXTERNAL |
| 114 | STATUS_RESET_SOFTWARE |
| 115 | STATUS_RESET_RESTORED |
| 116 | STATUS_RESET_CRASH |
| 117 | STATUS_RESET_ASSERT |
| 118 | STATUS_RESET_OTHER |
| 119 | STATUS_RESET_UNKNOWN |
| 120 | STATUS_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.
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_RESTOREin its reset form (seeCMD_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:
| Code | Name | Requires | Defined in |
|---|---|---|---|
| 8 | CAP_WRITABLE_RAW_STREAM | — | Frame Transport |
| 16 | CAP_PHY_DUTY_LIMIT | — | Radio Control |
| 32 | CAP_HOST_FILTER | — | Tethered Host Services |
| 33 | CAP_HOST_RX_QUEUE | CAP_HOST_FILTER | Tethered Host Services |
| 34 | CAP_HOST_KEYS | CAP_HOST_FILTER | Tethered Host Services |
| 35 | CAP_HOST_AUTO_ACK | CAP_HOST_KEYS, CAP_HOST_RX_QUEUE | Tethered Host Services |
| 36 | CAP_SAVE | — | Saved State |
| 37 | CAP_DEV_IDENTITY | — | Device Domain |
| 38 | CAP_DEV_NAME | — | Device Domain |
| 39 | CAP_BATTERY | — | Device Domain |
| 40 | CAP_REPEATER | CAP_DEV_IDENTITY | Device Domain |
| 41 | CAP_IDENT | CAP_DEV_IDENTITY | Device Domain |
| 42 | CAP_ALERT | — | Device Domain |
| 43 | CAP_ADMIN | CAP_DEV_IDENTITY | Node Management |
| 44 | CAP_TIME | — | Device Domain |
| 45 | CAP_GNSS | CAP_TIME | Device Domain |
| 46 | CAP_ADVERT | CAP_DEV_IDENTITY | Device Domain |
| 47 | CAP_ILLUMINANCE | — | Device Domain |
| 515 | CAP_PHY_LORA | — | Radio Control |
A device MUST NOT advertise a capability without also advertising the
capabilities it requires. 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.
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
| Code | Name | Grants |
|---|---|---|
| 16 | CAP_PHY_DUTY_LIMIT | Duty-cycle accounting and enforcement: PROP_PHY_DUTY_NOW, PROP_PHY_DUTY_LIMIT, and STATUS_DUTY_LIMIT |
| 515 | CAP_PHY_LORA | The LoRa modulation parameters: PROP_PHY_LORA_BW, PROP_PHY_LORA_SF, PROP_PHY_LORA_CR, PROP_PHY_LORA_SW |
Properties
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 32 | PROP_PHY_ENABLED | Get, Set | PHY enabled |
| 35 | PROP_PHY_FREQ | Get, Set | Frequency in kHz |
| 37 | PROP_PHY_TX_POWER | Get, Set | TX power in dBm |
| 38 | PROP_PHY_RSSI | Get | Current RSSI |
| 39 | PROP_PHY_LORA_BW | Get, Set | LoRa bandwidth |
| 40 | PROP_PHY_LORA_SF | Get, Set | LoRa spreading factor |
| 41 | PROP_PHY_LORA_CR | Get, Set | LoRa coding rate |
| 42 | PROP_PHY_MTU | Get | Max size of a frame |
| 43 | PROP_PHY_LORA_SW | Get, Set | LoRa sync word (16-bit style) |
| 4820 | PROP_PHY_DUTY_NOW | Get | Current duty usage |
| 4822 | PROP_PHY_DUTY_LIMIT | Get, Set | Duty-cycle limit |
PROP 32: PROP_PHY_ENABLED
- Type: Single-Value, Read/Write
- Asynchronous Updates: No
- Required:
CMD_PROP_GET: REQUIREDCMD_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.
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 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:
| Value | Percentage |
|---|---|
| 13107 | 20% |
| 6553 | 10% |
| 655 | 1% |
| 65 | 0.1% |
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
| Code | Name | Grants |
|---|---|---|
| 8 | CAP_WRITABLE_RAW_STREAM | STR_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
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 9 | CMD_STR_SEND | Host->Device | Send data to a stream |
| 10 | CMD_STR_RECV | Device->Host | Receive 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
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 113 | STR_PHY_RAW | Send, Recv | Raw 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 (0x7Findicates to use the radio default,0x7Eindicates to transmit at maximum power). A power the radio cannot reach is clamped to its range, as forPROP_PHY_TX_POWER.TX_FLAGS(u8): Transmit flagsTX_FLAG_NOCCABit 0: If set, do not use CCA (or the equivalent LoRa mechanism)TX_FLAG_NODUTYBit 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.
- If
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.
- If
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 (unlike0xFFFF, which is-0.1 dB).
- If
Buffered-Frame Metadata
On a device that supports inbound queueing
(CAP_HOST_RX_QUEUE), the Recv metadata is extended with two trailing
fields:
RX_FLAGS(u8): Buffered-frame flagsRX_FLAG_BUFFEREDBit 0: The frame was held in the inbound queue and is being delivered byCMD_QUEUE_DRAIN.RX_FLAG_ACKEDBit 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.- 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. Live deliveries MAY therefore continue to 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.
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).
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 37 | CAP_DEV_IDENTITY | — | The device identity: PROP_DEV_KEY, PROP_DEV_PRIVATE_KEY, PROP_DEV_CHANNEL_KEYS, PROP_DEV_PEERS |
| 38 | CAP_DEV_NAME | — | PROP_DEV_NAME |
| 39 | CAP_BATTERY | — | Battery-powered operation and PROP_BATTERY |
| 40 | CAP_REPEATER | CAP_DEV_IDENTITY | Autonomous 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 |
| 41 | CAP_IDENT | CAP_DEV_IDENTITY | PROP_IDENT, PROP_IDENT_ROLE, PROP_IDENT_MOBILE — serving and configuring the device identity’s advertised node identity |
| 42 | CAP_ALERT | — | Some means of making the device physically conspicuous on demand, and PROP_ALERT |
| 44 | CAP_TIME | — | A wall clock: PROP_TIME, PROP_TZ_OFFSET |
| 45 | CAP_GNSS | CAP_TIME | A 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 |
| 46 | CAP_ADVERT | CAP_DEV_IDENTITY | Announcing itself on a schedule of its own: PROP_ADVERT_INTERVAL, PROP_BEACON_INTERVAL, PROP_STARTUP_BEACON |
| 47 | CAP_ILLUMINANCE | — | An ambient light sensor and PROP_ILLUMINANCE |
CAP_ADVERT requires CAP_DEV_IDENTITY because what a scheduled
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.
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 the advertisement policy — 80–82 allocated, 83–87 reserved — 88–93 are positioning (88 the receiver switch, 89–93 the fix telemetry), and 94–95 are environmental sensing: 94 illuminance, 95 reserved.
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–4867.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 64 | PROP_DEV_KEY | Get | Device identity public key |
| 65 | PROP_DEV_PRIVATE_KEY | Set | Device identity private key (write-only) |
| 66 | PROP_DEV_CHANNEL_KEYS | Get, Set, Insert, Remove | Device identity channel keys |
| 67 | PROP_DEV_PEERS | Get, Set, Insert, Remove | Device identity peer list |
| 68 | PROP_DEV_NAME | Get, Set | Human-readable device name |
| 69 | PROP_BATTERY | Get, Is | Battery status snapshot |
| 70 | PROP_MAC_REPEATER_ENABLED | Get, Set | Autonomous repeater forwarding enable |
| 71 | PROP_IDENT | Get | Signed node identity of the device identity |
| 72 | PROP_IDENT_ROLE | Get, Set | Advertised node role, or empty to derive it |
| 73 | PROP_IDENT_MOBILE | Get, Set | Advertise the mobile capability bit |
| 74 | PROP_MAC_REPEATER_REGIONS | Get, Set | Region codes the device forwards for |
| 75 | PROP_MAC_REPEATER_DEFAULT_REGION | Get, Set | Region code inserted into untagged flood packets |
| 76 | PROP_MAC_REPEATER_MIN_RSSI | Get, Set | Minimum received RSSI for flood forwarding |
| 77 | PROP_MAC_REPEATER_MIN_SNR | Get, Set | Minimum received SNR for flood forwarding |
| 78 | PROP_DEV_DISCOVERABLE | Get, Set | Whether the device identity answers Identity Requests |
| 79 | PROP_ALERT | Get, Set, Is | Locate alert state |
| 80 | PROP_ADVERT_INTERVAL | Get, Set | Seconds between unsolicited advertisements |
| 81 | PROP_BEACON_INTERVAL | Get, Set | Seconds between unsolicited beacons |
| 82 | PROP_STARTUP_BEACON | Get, Set | Whether a beacon goes out at bring-up |
| 88 | PROP_GNSS_ENABLED | Get, Set | Whether the GNSS receiver is powered |
| 89 | PROP_GNSS_LOCATION | Get, Is | Position of the last fix |
| 90 | PROP_GNSS_ALTITUDE | Get | Altitude of the last fix |
| 91 | PROP_GNSS_FIX | Get, Is | Fix quality |
| 92 | PROP_GNSS_PRECISION | Get | Estimated horizontal accuracy of the last fix |
| 93 | PROP_GNSS_SATELLITES | Get | Satellites used, and optionally in view |
| 94 | PROP_ILLUMINANCE | Get | Ambient illuminance in millilux |
| 4866 | PROP_TIME | Get, Set, Is | Wall clock, or empty when unknown |
| 4867 | PROP_TZ_OFFSET | Get, Set | Local time-zone offset from UTC |
| 4868 | PROP_GNSS_IDENT_UPDATE | Get, Set | Whether fixes update the advertised node identity |
| 4869 | PROP_GNSS_IDENT_PRECISION | Get, Set | Precision the advertised location is clamped to |
| 4870 | PROP_GNSS_TIME_TRUST | Get, Set | Whether receiver-derived time may set the clock |
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)
- 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:
| Octets | Field |
|---|---|
| 1 | Field flags |
| 0 or 2 | Battery voltage, UINT16_LE, millivolts |
| 0 or 1 | Battery 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:
| Value | Name |
|---|---|
| 0 | BATTERY_CHARGE_STATE_DISCHARGING |
| 1 | BATTERY_CHARGE_STATE_CHARGING |
| 2 | BATTERY_CHARGE_STATE_CHARGED |
BATTERY_CHARGE_STATE_DISCHARGING- The charging system reports neither active charging nor charge completion. This is the charge state used for a disconnected battery when the implementation can detect that condition; 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 charging system reports that charging has
completed. A battery at 100 percent while operating without external power
remains in
BATTERY_CHARGE_STATE_DISCHARGING.
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–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 overheard routable
frames according to Repeater Operation, and it
sets the repeater capability bit in its node
identity. When false, the device identity does not
forward 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.
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 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 74: PROP_MAC_REPEATER_REGIONS
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_REPEATER - Value Type: Concatenated 2-octet region codes
- Post-Reset Value: Empty, or restored from saved state
The set of regions the device identity flood-forwards for, as the codes
themselves concatenated with no delimiter — byte-for-byte the encoding of
the Supported Regions
identity option. The value length is therefore always even; a device
MUST reject an odd-length write with STATUS_INVALID_ARGUMENT, and
MAY reject a write that exceeds the number of entries it can hold.
The list is 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 appears here. An empty list — the factory default — imposes no regional restriction, so a tagged packet is forwarded whatever its region.
A device with forwarding enabled and a non-empty list SHOULD advertise the same codes in its node identity, 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. 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.
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.
| Value | Name |
|---|---|
| 0 | ALERT_NONE |
| 1 | ALERT_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 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:
- The host writes
ALERT_NONE. - 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.
- The deadline expires. A device MUST bound how long it will remain
in
ALERT_LOCATE; a few minutes is RECOMMENDED. WritingALERT_LOCATEwhile 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 signed node identity. 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, it carries its source in full-key form (§Node Identity).
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.
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 both the Trace Route and Trace Signal options, 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 neighbours 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 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 in this revision. A manually-placed fixed node is a real use, but writing a position requires a rule for which source wins when the receiver also has one, and this revision does not define that rule.
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.
| Value | Meaning |
|---|---|
| 0 | No fix |
| 1 | Two-dimensional fix — position without altitude |
| 2 | Three-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 secondUINT8 - 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.
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 4867: PROP_TZ_OFFSET
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_TIME - Value Type:
INT16_LE - Post-Reset Value: 0, or restored from saved state
The local time-zone offset from UTC, in minutes east of UTC. Negative values are west.
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 minute offset rather than an hour one, because several real zones are
not whole hours. Values outside the range of real civil offsets
(−720 through +840) are rejected with STATUS_INVALID_ARGUMENT: outside
it, a value is a unit or byte-order mistake, and a device that accepted
one would display a confidently wrong local time.
Carries an offset and not a zone identifier. Devices do not carry a zone database, so daylight-saving transitions are the host’s business: whatever adjusts the device’s clock adjusts its offset.
PROP 4868: PROP_GNSS_IDENT_UPDATE
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- 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.
Switching it off retracts the advertised position rather than freezing the last one. So does switching the receiver off. A position that nothing is refreshing any more is a claim the device cannot support, and it ages into a false one at whatever speed the device moves; a device therefore drops the location and the altitude together when it stops updating them.
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: No
- 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.
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.
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
| Code | Name | Grants |
|---|---|---|
| 36 | CAP_SAVE | CMD_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 queueing and
acknowledging on the host’s behalf.
-
CMD_SAVEatomically writes the current device domain configuration — including the RF configuration and the current value ofPROP_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 (seePROP_DEV_PRIVATE_KEY) and is changed only by explicit provisioning orCMD_CLEAR— neitherCMD_RESTOREnor a reboot can revert the device identity to an earlier key.PROP_TIMEis 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 is ordinary configuration and is saved. -
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_RESTOREreverts 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_CLEARerases the snapshot and all other persisted provisioning, including the device identity private key. It does not modify live (in-RAM) state; a subsequentCMD_RSTcompletes a factory reset. Transport-level state such as BLE bonds is not affected. -
PROP_SAVEDreports 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_RSTreverts 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 byCMD_CLEARfollowed byCMD_RST. A host that expects documented defaults afterCMD_RSTwill 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_SAVEDMUST report a fallback and an unreadable snapshot distinguishably from both “saved” and “nothing saved” (seePROP_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_SAVEreplaces 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.
Commands
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 12 | CMD_SAVE | Host->Device | Save state to non-volatile storage |
| 13 | CMD_CLEAR | Host->Device | Erase all saved state |
| 14 | CMD_RESTORE | Host->Device | Restore state from the saved snapshot |
| 15 | CMD_FACTORY_RESET | Host->Device | Erase 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. 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 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 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 unsolicitedCMD_PROP_ISforPROP_LAST_STATUScarrying the reset codeSTATUS_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 asPROP_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 withCMD_PROP_ISforPROP_LAST_STATUScarryingSTATUS_OKand 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 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.
Properties
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 49 | PROP_SAVED | Get | Saved-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:
| Value | Meaning |
|---|---|
| 0 | Nothing is saved. Every property holds its documented default. |
| 1 | The most recently saved snapshot is in effect. |
| 2 | A 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. |
| 3 | A 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
- Values 2 and 3 persist for the remainder of the power cycle and
MUST NOT be cleared by
CMD_RSTorCMD_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 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.
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 32 | CAP_HOST_FILTER | — | PROP_HOST_KEY, PROP_MAC_PROMISCUOUS, PROP_HOST_RX_FILTERS, and the receive-filtering behavior |
| 33 | CAP_HOST_RX_QUEUE | CAP_HOST_FILTER | The inbound queue, its properties, CMD_QUEUE_DRAIN, and the buffered-frame metadata |
| 34 | CAP_HOST_KEYS | CAP_HOST_FILTER | PROP_HOST_CHANNEL_KEYS and PROP_HOST_PEER_KEYS |
| 35 | CAP_HOST_AUTO_ACK | CAP_HOST_KEYS, CAP_HOST_RX_QUEUE | PROP_HOST_AUTO_ACK and acknowledgement delegation |
A device advertising none of these is a transparent radio: it delivers every frame it receives and holds nothing on anyone’s behalf.
Commands
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 11 | CMD_QUEUE_DRAIN | Host->Device | Deliver 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.
PROP_MAC_PROMISCUOUS is the exception: it is a live diagnostic mode
rather than provisioning, and is the protocol’s only session-scoped
property.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 48 | PROP_MAC_PROMISCUOUS | Get, Set | Deliver all frames (session-scoped) |
| 96 | PROP_HOST_KEY | Get, Set | Tethered host identity public key |
| 97 | PROP_HOST_CHANNEL_KEYS | Get, Set, Insert, Remove | Host channel keys |
| 98 | PROP_HOST_PEER_KEYS | Get, Set, Insert, Remove | Host pairwise peer keys |
| 99 | PROP_HOST_RX_FILTERS | Get, Set, Insert, Remove | Host receive filter table |
| 100 | PROP_HOST_AUTO_ACK | Get, Set | Acknowledgement delegation enable |
| 101 | PROP_HOST_RX_QUEUE_COUNT | Get | Frames currently queued |
| 102 | PROP_HOST_RX_QUEUE_CAPACITY | Get, Set | Queue capacity in frames |
| 103 | PROP_HOST_RX_QUEUE_DROPPED | Get | Frames 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.
This is the only session-scoped property: 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)
- 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, 64 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 16 B 16 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. 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 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
| Type | Name | Value | Matches |
|---|---|---|---|
| 0 | FILTER_DEST_HINT | 3 octets | Frames whose destination hint field equals the value |
| 1 | FILTER_CHANNEL_ID | 2 octets | Channel-addressed frames (MCST, BUNI, BUAR) whose channel identifier equals the value |
| 2 | FILTER_PKT_TYPE | 1 octet | Frames 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_SETsupport 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 MIC 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 MIC while
its destination hint names the remote peer. The device therefore records the
leading 4 MIC bytes of each frame it transmits on the host’s behalf and
implicitly accepts any received frame whose trailer opens with a recorded
value — for a MAC Ack this matches the returning acknowledgement, and for
other packet types it matches the host’s own send being carried onward, which
the host’s forwarding-confirmation machinery must overhear 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 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.
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:
PROP_HOST_AUTO_ACKis true and no host is attached.- The frame’s packet type requests acknowledgement:
UNAR, orBUARwhere the device also holds the frame’s channel key. - The frame is addressed to the host identity: its (possibly decrypted)
destination hint matches
PROP_HOST_KEY, and its source resolves to an entry inPROP_HOST_PEER_KEYS— by full public key when theSflag is set, or by unique 3-byte prefix match otherwise. - The frame authenticates: its MIC verifies under the pairwise
K_MICforUNAR, or under the combined blind unicast payload keys forBUAR. - 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.
- 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.
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.
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
| Id | Mnemonic | Required |
|---|---|---|
| 0 | CMD_NOP | Always |
| 1 | CMD_RST | Always |
| 2 | CMD_PROP_GET | Always |
| 3 | CMD_PROP_SET | Always |
| 6 | CMD_PROP_IS | Always |
| 9 | CMD_STR_SEND | Always |
| 10 | CMD_STR_RECV | Always |
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; CMD_CLEAR and
CMD_FACTORY_RESET are available regardless of capabilities (see
Saved State).
Properties
| Id | Mnemonic | Required |
|---|---|---|
| 0 | PROP_LAST_STATUS | Always |
| 1 | PROP_PROTOCOL_VERSION | Always |
| 2 | PROP_DEV_VERSION | Always |
| 3 | PROP_INTERFACE_TYPE | Always |
| 5 | PROP_CAPS | Always |
| 32 | PROP_PHY_ENABLED | Always, Get and Set |
| 35 | PROP_PHY_FREQ | Always |
| 37 | PROP_PHY_TX_POWER | Always |
| 38 | PROP_PHY_RSSI | Always |
| 42 | PROP_PHY_MTU | Always |
| 113 | STR_PHY_RAW | Always |
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.
| Subsystem | Capabilities |
|---|---|
| Radio Control beyond the required properties | CAP_PHY_LORA, CAP_PHY_DUTY_LIMIT |
| Device Domain | CAP_DEV_IDENTITY, CAP_DEV_NAME, CAP_BATTERY, CAP_REPEATER, CAP_IDENT, CAP_ALERT, CAP_TIME, CAP_GNSS, CAP_ADVERT, CAP_ILLUMINANCE |
| Saved State | CAP_SAVE |
| Tethered Host Services | CAP_HOST_FILTER, CAP_HOST_KEYS, CAP_HOST_RX_QUEUE, CAP_HOST_AUTO_ACK |
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, andCMD_PROP_REMOVEDnotifications 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_ISas the property’s value, and MUST NOT treat one that differs from what it wrote as an error. A write is refused by aPROP_LAST_STATUScarrying 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 — seePROP_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:
| Deployment | Typical capabilities |
|---|---|
| Transparent radio | CAP_WRITABLE_RAW_STREAM, CAP_PHY_LORA, CAP_PHY_DUTY_LIMIT |
| Companion radio | The above, plus CAP_HOST_FILTER, CAP_HOST_KEYS, CAP_HOST_RX_QUEUE, CAP_HOST_AUTO_ACK |
| Commissioned repeater | The 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
| Id | Mnemonic | Dir | Gated by |
|---|---|---|---|
| 0 | CMD_NOP | Host->Device | — |
| 1 | CMD_RST | Host->Device | — |
| 2 | CMD_PROP_GET | Host->Device | — |
| 3 | CMD_PROP_SET | Host->Device | — |
| 4 | CMD_PROP_INSERT | Host->Device | — |
| 5 | CMD_PROP_REMOVE | Host->Device | — |
| 6 | CMD_PROP_IS | Device->Host | — |
| 7 | CMD_PROP_INSERTED | Device->Host | — |
| 8 | CMD_PROP_REMOVED | Device->Host | — |
| 9 | CMD_STR_SEND | Host->Device | — |
| 10 | CMD_STR_RECV | Device->Host | — |
| 11 | CMD_QUEUE_DRAIN | Host->Device | CAP_HOST_RX_QUEUE |
| 12 | CMD_SAVE | Host->Device | CAP_SAVE |
| 13 | CMD_CLEAR | Host->Device | — |
| 14 | CMD_RESTORE | Host->Device | CAP_SAVE |
| 15 | CMD_FACTORY_RESET | Host->Device | — |
Command identifiers are 7-bit; 16–127 are unassigned.
Properties and Streams
Identifiers are allocated by state class; see Property Allocation.
| Id | Mnemonic | Commands | Gated by |
|---|---|---|---|
| 0 | PROP_LAST_STATUS | Get, Is | — |
| 1 | PROP_PROTOCOL_VERSION | Get | — |
| 2 | PROP_DEV_VERSION | Get | — |
| 3 | PROP_INTERFACE_TYPE | Get | — |
| 5 | PROP_CAPS | Get | — |
| 32 | PROP_PHY_ENABLED | Get, Set | — |
| 35 | PROP_PHY_FREQ | Get, Set | — |
| 37 | PROP_PHY_TX_POWER | Get, Set | — |
| 38 | PROP_PHY_RSSI | Get | — |
| 39 | PROP_PHY_LORA_BW | Get, Set | CAP_PHY_LORA |
| 40 | PROP_PHY_LORA_SF | Get, Set | CAP_PHY_LORA |
| 41 | PROP_PHY_LORA_CR | Get, Set | CAP_PHY_LORA |
| 42 | PROP_PHY_MTU | Get | — |
| 43 | PROP_PHY_LORA_SW | Get, Set | CAP_PHY_LORA |
| 48 | PROP_MAC_PROMISCUOUS | Get, Set | CAP_HOST_FILTER |
| 49 | PROP_SAVED | Get | CAP_SAVE |
| 64 | PROP_DEV_KEY | Get | CAP_DEV_IDENTITY |
| 65 | PROP_DEV_PRIVATE_KEY | Set | CAP_DEV_IDENTITY |
| 66 | PROP_DEV_CHANNEL_KEYS | Get, Set, Insert, Remove | CAP_DEV_IDENTITY |
| 67 | PROP_DEV_PEERS | Get, Set, Insert, Remove | CAP_DEV_IDENTITY |
| 68 | PROP_DEV_NAME | Get, Set | CAP_DEV_NAME |
| 69 | PROP_BATTERY | Get, Is | CAP_BATTERY |
| 70 | PROP_MAC_REPEATER_ENABLED | Get, Set | CAP_REPEATER |
| 71 | PROP_IDENT | Get | CAP_IDENT |
| 72 | PROP_IDENT_ROLE | Get, Set | CAP_IDENT |
| 73 | PROP_IDENT_MOBILE | Get, Set | CAP_IDENT |
| 74 | PROP_MAC_REPEATER_REGIONS | Get, Set | CAP_REPEATER |
| 75 | PROP_MAC_REPEATER_DEFAULT_REGION | Get, Set | CAP_REPEATER |
| 76 | PROP_MAC_REPEATER_MIN_RSSI | Get, Set | CAP_REPEATER |
| 77 | PROP_MAC_REPEATER_MIN_SNR | Get, Set | CAP_REPEATER |
| 78 | PROP_DEV_DISCOVERABLE | Get, Set | CAP_DEV_IDENTITY |
| 79 | PROP_ALERT | Get, Set, Is | CAP_ALERT |
| 80 | PROP_ADVERT_INTERVAL | Get, Set | CAP_ADVERT |
| 81 | PROP_BEACON_INTERVAL | Get, Set | CAP_ADVERT |
| 82 | PROP_STARTUP_BEACON | Get, Set | CAP_ADVERT |
| 88 | PROP_GNSS_ENABLED | Get, Set | CAP_GNSS |
| 89 | PROP_GNSS_LOCATION | Get, Is | CAP_GNSS |
| 90 | PROP_GNSS_ALTITUDE | Get | CAP_GNSS |
| 91 | PROP_GNSS_FIX | Get, Is | CAP_GNSS |
| 92 | PROP_GNSS_PRECISION | Get | CAP_GNSS |
| 93 | PROP_GNSS_SATELLITES | Get | CAP_GNSS |
| 94 | PROP_ILLUMINANCE | Get | CAP_ILLUMINANCE |
| 96 | PROP_HOST_KEY | Get, Set | CAP_HOST_FILTER |
| 97 | PROP_HOST_CHANNEL_KEYS | Get, Set, Insert, Remove | CAP_HOST_KEYS |
| 98 | PROP_HOST_PEER_KEYS | Get, Set, Insert, Remove | CAP_HOST_KEYS |
| 99 | PROP_HOST_RX_FILTERS | Get, Set, Insert, Remove | CAP_HOST_FILTER |
| 100 | PROP_HOST_AUTO_ACK | Get, Set | CAP_HOST_AUTO_ACK |
| 101 | PROP_HOST_RX_QUEUE_COUNT | Get | CAP_HOST_RX_QUEUE |
| 102 | PROP_HOST_RX_QUEUE_CAPACITY | Get, Set | CAP_HOST_RX_QUEUE |
| 103 | PROP_HOST_RX_QUEUE_DROPPED | Get | CAP_HOST_RX_QUEUE |
| 113 | STR_PHY_RAW | Send, Recv | — |
| 4820 | PROP_PHY_DUTY_NOW | Get | CAP_PHY_DUTY_LIMIT |
| 4822 | PROP_PHY_DUTY_LIMIT | Get, Set | CAP_PHY_DUTY_LIMIT |
| 4864 | PROP_BLE_PAIRING_PIN | Set | BLE transport |
| 4865 | PROP_DEV_ADMINS | Get, Set, Insert, Remove | CAP_ADMIN |
| 4866 | PROP_TIME | Get, Set, Is | CAP_TIME |
| 4867 | PROP_TZ_OFFSET | Get, Set | CAP_TIME |
| 4868 | PROP_GNSS_IDENT_UPDATE | Get, Set | CAP_GNSS |
| 4869 | PROP_GNSS_IDENT_PRECISION | Get, Set | CAP_GNSS |
| 4870 | PROP_GNSS_TIME_TRUST | Get, Set | CAP_GNSS |
Capabilities
Advertised through PROP_CAPS; the allocation
is in Capabilities.
| Code | Name | Defined in |
|---|---|---|
| 8 | CAP_WRITABLE_RAW_STREAM | Frame Transport |
| 16 | CAP_PHY_DUTY_LIMIT | Radio Control |
| 32 | CAP_HOST_FILTER | Tethered Host Services |
| 33 | CAP_HOST_RX_QUEUE | Tethered Host Services |
| 34 | CAP_HOST_KEYS | Tethered Host Services |
| 35 | CAP_HOST_AUTO_ACK | Tethered Host Services |
| 36 | CAP_SAVE | Saved State |
| 37 | CAP_DEV_IDENTITY | Device Domain |
| 38 | CAP_DEV_NAME | Device Domain |
| 39 | CAP_BATTERY | Device Domain |
| 40 | CAP_REPEATER | Device Domain |
| 41 | CAP_IDENT | Device Domain |
| 42 | CAP_ALERT | Device Domain |
| 43 | CAP_ADMIN | Node Management |
| 44 | CAP_TIME | Device Domain |
| 45 | CAP_GNSS | Device Domain |
| 46 | CAP_ADVERT | Device Domain |
| 47 | CAP_ILLUMINANCE | Device Domain |
| 515 | CAP_PHY_LORA | Radio Control |
Status Codes
Defined in Status Codes.
| Id | Name | Id | Name |
|---|---|---|---|
| 0 | STATUS_OK | 12 | STATUS_BUSY |
| 1 | STATUS_FAILURE | 13 | STATUS_PROP_NOT_FOUND |
| 2 | STATUS_UNIMPLEMENTED | 18 | STATUS_CCA_FAILURE |
| 3 | STATUS_INVALID_ARGUMENT | 19 | STATUS_ALREADY |
| 4 | STATUS_INVALID_STATE | 20 | STATUS_ITEM_NOT_FOUND |
| 5 | STATUS_INVALID_COMMAND | 21 | STATUS_CURSOR_INVALID |
| 7 | STATUS_INTERNAL_ERROR | 32 | STATUS_DUTY_LIMIT |
| 9 | STATUS_PARSE_ERROR | ||
| 10 | STATUS_IN_PROGRESS | ||
| 11 | STATUS_NOMEM |
Reset Codes
Defined in Reset Codes; the range 112–127 is reserved for them.
| Id | Name | Id | Name |
|---|---|---|---|
| 112 | STATUS_RESET_POWER_ON | 117 | STATUS_RESET_ASSERT |
| 113 | STATUS_RESET_EXTERNAL | 118 | STATUS_RESET_OTHER |
| 114 | STATUS_RESET_SOFTWARE | 119 | STATUS_RESET_UNKNOWN |
| 115 | STATUS_RESET_RESTORED | 120 | STATUS_RESET_WATCHDOG |
| 116 | STATUS_RESET_CRASH |
Enumerated Values
The value enumerations carried inside properties and stream metadata.
| Enumeration | Values | Defined in |
|---|---|---|
PROP_SAVED | 0 none, 1 current, 2 fallback, 3 unreadable | PROP_SAVED |
| Filter types | 0 FILTER_DEST_HINT, 1 FILTER_CHANNEL_ID, 2 FILTER_PKT_TYPE | PROP_HOST_RX_FILTERS |
| Charge states | 0 discharging, 1 charging, 2 charged | PROP_BATTERY |
| Alert states | 0 ALERT_NONE, 1 ALERT_LOCATE | PROP_ALERT |
| Fix quality | 0 none, 1 two-dimensional, 2 three-dimensional | PROP_GNSS_FIX |
| Transmit flags | bit 0 TX_FLAG_NOCCA, bit 1 TX_FLAG_NODUTY | STR_PHY_RAW |
| Receive flags | bit 0 RX_FLAG_BUFFERED, bit 1 RX_FLAG_ACKED | Buffered-Frame Metadata |
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:
| Characteristic | Direction | GATT Properties |
|---|---|---|
| Frame In | Client→Server | Write; Write Without Response (optional) |
| Frame Out | Server→Client | Notify |
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:
| Value | Name | Meaning |
|---|---|---|
| 0 | SAR_COMPLETE | The segment contains a complete frame |
| 1 | SAR_FIRST | First segment of a segmented frame |
| 2 | SAR_CONT | Continuation segment of a segmented frame |
| 3 | SAR_LAST | Last 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. OnSAR_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.
| Slot | UUID | Assignment |
|---|---|---|
0x0001 | 21EB6B15-0001-4CCF-92E4-A079171BEC97 | ULCP GATT Service |
0x0002 | 21EB6B15-0002-4CCF-92E4-A079171BEC97 | Frame In characteristic |
0x0003 | 21EB6B15-0003-4CCF-92E4-A079171BEC97 | Frame 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
While powered and not attached, the device SHOULD advertise as
connectable and include the ULCP GATT Service UUID in its
advertising data or scan response, so hosts can discover devices
by service rather than by name. The advertised local name is
implementation-specific unless the device advertises CAP_DEV_NAME. Such a device
SHOULD use its current PROP_DEV_NAME as the advertised local name,
shortening it without splitting a UTF-8 code point when the advertising or scan
response payload cannot hold the complete value. A name changed while a BLE
connection is active takes effect on the next advertising cycle; changing it
does not require disconnecting the attached host.
While a host is attached over another transport (for example, an open ULCP session over USB-CDC), the device SHOULD suspend advertising, and SHOULD resume it when that host detaches.
Advertising content MUST NOT reveal whether the device holds bonds or identify previously bonded hosts. Devices SHOULD use resolvable private addresses.
Pairing mode (see Pairing Mode) governs only the acceptance of pairing requests; it does not affect advertising. In particular, a bonded device continues to advertise outside pairing mode so that its bonded hosts can reconnect.
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 itself (approximately 128-bit; 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 for a short window (15–30 seconds RECOMMENDED).
- Once the device holds one or more bonds, it MUST NOT enter pairing mode automatically. Entering pairing mode then requires 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.
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.
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 but MUST NOT be invocable through ULCP itself over an unauthenticated path.
- 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 currently
expressible through this protocol; the local mechanism above and
CMD_FACTORY_RESET are the available paths.
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:
- 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.
- Order the bytes from least-significant to most-significant. (Little-endian)
- 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:
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 0 | CMD_NOP | Host->Device | No-Operation |
| 1 | CMD_RST | Host->Device | Reset the device |
| 2 | CMD_PROP_GET | Host->Device | Get property value |
| 3 | CMD_PROP_SET | Host->Device | Set property value |
| 6 | CMD_PROP_IS | Device->Host | Property value notification |
| 9 | CMD_STR_SEND | Host->Device | Send data to a stream |
| 10 | CMD_STR_RECV | Device->Host | Receive 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
Ismeans that the property may be emitted asynchronously. All properties that supportGetorSetwill emit anIsto respond with the current/new value of that property.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 0 | PROP_LAST_STATUS | Get, Is | Last status |
| 1 | PROP_PROTOCOL_VERSION | Get | Protocol version |
| 2 | PROP_DEV_VERSION | Get | Device version string |
| 3 | PROP_IFACE_TYPE | Get | Interface type |
| 5 | PROP_CAPS | Get | Capabilities |
| 32 | PROP_PHY_ENABLED | Get, Set | PHY enabled |
| 35 | PROP_PHY_FREQ | Get, Set | Frequency in kHz |
| 37 | PROP_PHY_TX_POWER | Get, Set | TX power in dBm |
| 38 | PROP_PHY_RSSI | Get | Current RSSI |
| 39 | PROP_PHY_LORA_BW | Get, Set | LoRa bandwidth |
| 40 | PROP_PHY_LORA_SF | Get, Set | LoRa spreading factor |
| 41 | PROP_PHY_LORA_CR | Get, Set | LoRa coding rate |
| 42 | PROP_PHY_MTU | Get | Max size of a frame |
| 43 | PROP_PHY_LORA_SW | Get, Set | LoRa sync word (16-bit style) |
| 113 | STR_PHY_RAW | Send, Recv | Raw radio frame stream |
| 4820 | PROP_PHY_DUTY_NOW | Get | Current duty usage |
| 4822 | PROP_PHY_DUTY_LIMIT | Get, Set | Duty-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: REQUIREDCMD_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 (0x7Findicates to use the radio default,0x7Eindicates to transmit at maximum power)TX_FLAGS(u8): Transmit flagsTX_FLAG_NOCCABit 0: If set, do not use CCA (or the equivalent LoRa mechanism)TX_FLAG_NODUTYBit 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.
- If
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.
- If
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 (unlike0xFFFF, which is-0.1 dB).
- If
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:
| Value | Percentage |
|---|---|
| 13107 | 20% |
| 6553 | 10% |
| 655 | 1% |
| 65 | 0.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.
| Id | Name |
|---|---|
| 0 | STATUS_OK |
| 1 | STATUS_FAILURE |
| 2 | STATUS_UNIMPLEMENTED |
| 3 | STATUS_INVALID_ARGUMENT |
| 4 | STATUS_INVALID_STATE |
| 5 | STATUS_INVALID_COMMAND |
| 7 | STATUS_INTERNAL_ERROR |
| 9 | STATUS_PARSE_ERROR |
| 10 | STATUS_IN_PROGRESS |
| 11 | STATUS_NOMEM |
| 12 | STATUS_BUSY |
| 13 | STATUS_PROP_NOT_FOUND |
| 18 | STATUS_CCA_FAILURE |
| 32 | STATUS_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_STATEin 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.
| Id | Name |
|---|---|
| 112 | STATUS_RESET_POWER_ON |
| 113 | STATUS_RESET_EXTERNAL |
| 114 | STATUS_RESET_SOFTWARE |
| 116 | STATUS_RESET_CRASH |
| 117 | STATUS_RESET_ASSERT |
| 118 | STATUS_RESET_OTHER |
| 119 | STATUS_RESET_UNKNOWN |
| 120 | STATUS_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.
| Code | Name |
|---|---|
| 8 | CAP_WRITABLE_RAW_STREAM |
| 16 | CAP_PHY_DUTY_LIMIT |
| 515 | CAP_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_IDENTITYthat 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_KEYtherefore 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, andPROP_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_ENABLEDis 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 session-scoped properties —
currently only PROP_MAC_PROMISCUOUS. 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_*), includingPROP_PHY_ENABLED, and the duty-cycle limit - the human-readable device name (
PROP_DEV_NAME) - live battery telemetry (
PROP_BATTERY), whenCAP_BATTERYis 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_KEYitself- 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:
CMD_PROP_GETforPROP_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.CMD_PROP_GETforPROP_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.CMD_PROP_GETfor the device-domain properties the host depends on (PROP_SAVED, thePROP_PHY_*configuration), and forPROP_HOST_RX_QUEUE_COUNT.- 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. - Issue
CMD_QUEUE_DRAINwhen 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 ofPROP_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 orCMD_CLEAR— neitherCMD_RESTOREnor 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 subsequentCMD_RSTcompletes 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_RSTreverts 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 byCMD_CLEARfollowed byCMD_RST. A host that implements only the minimal protocol and expects documented defaults afterCMD_RSTwill 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_SAVEDMUST 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_SAVEreplaces 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:
| Id | Mnemonic | Dir | Description |
|---|---|---|---|
| 4 | CMD_PROP_INSERT | Host->Device | Insert an item into a multi-value property |
| 5 | CMD_PROP_REMOVE | Host->Device | Remove an item from a multi-value property |
| 7 | CMD_PROP_INSERTED | Device->Host | Item-inserted notification |
| 8 | CMD_PROP_REMOVED | Device->Host | Item-removed notification |
| 11 | CMD_QUEUE_DRAIN | Host->Device | Deliver queued inbound frames |
| 12 | CMD_SAVE | Host->Device | Save state to non-volatile storage |
| 13 | CMD_CLEAR | Host->Device | Erase all saved state |
| 14 | CMD_RESTORE | Host->Device | Restore state from the saved snapshot |
| 15 | CMD_FACTORY_RESET | Host->Device | Erase 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 and host 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 unsolicitedCMD_PROP_ISforPROP_LAST_STATUScarrying the reset codeSTATUS_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 asPROP_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 withCMD_PROP_ISforPROP_LAST_STATUScarryingSTATUS_OKand 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.
This command is only available on devices advertising CAP_SAVE; otherwise
it fails with STATUS_UNIMPLEMENTED.
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 withCMD_PROP_ISwhose 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 aCMD_PROP_IScarrying 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_SETwhose 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 settingPROP_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, orCMD_PROP_REMOVEDnotifications 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:
| Range | Class |
|---|---|
| 48–63 | Session-scoped and global protocol state |
| 64–95 | Device domain |
| 96–127 | Host domain (PROP_HOST_*) |
Unassigned identifiers in these ranges are reserved.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 48 | PROP_MAC_PROMISCUOUS | Get, Set | Deliver all frames (session-scoped) |
| 49 | PROP_SAVED | Get | Saved-snapshot state |
| 64 | PROP_DEV_KEY | Get | Device identity public key |
| 65 | PROP_DEV_PRIVATE_KEY | Set | Device identity private key (write-only) |
| 66 | PROP_DEV_CHANNEL_KEYS | Get, Set, Insert, Remove | Device identity channel keys |
| 67 | PROP_DEV_PEERS | Get, Set, Insert, Remove | Device identity peer list |
| 68 | PROP_DEV_NAME | Get, Set | Human-readable device name |
| 69 | PROP_BATTERY | Get, Is | Battery status snapshot |
| 70 | PROP_MAC_REPEATER_ENABLED | Get, Set | Autonomous repeater forwarding enable |
| 71 | PROP_IDENT | Get | Signed node identity of the device identity |
| 72 | PROP_IDENT_ROLE | Get, Set | Advertised node role, or empty to derive it |
| 73 | PROP_IDENT_MOBILE | Get, Set | Advertise the mobile capability bit |
| 96 | PROP_HOST_KEY | Get, Set | Tethered host identity public key |
| 97 | PROP_HOST_CHANNEL_KEYS | Get, Set, Insert, Remove | Host channel keys |
| 98 | PROP_HOST_PEER_KEYS | Get, Set, Insert, Remove | Host pairwise peer keys |
| 99 | PROP_HOST_RX_FILTERS | Get, Set, Insert, Remove | Host receive filter table |
| 100 | PROP_HOST_AUTO_ACK | Get, Set | Acknowledgement delegation enable |
| 101 | PROP_HOST_RX_QUEUE_COUNT | Get | Frames currently queued |
| 102 | PROP_HOST_RX_QUEUE_CAPACITY | Get, Set | Queue capacity in frames |
| 103 | PROP_HOST_RX_QUEUE_DROPPED | Get | Frames 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.
This is the only session-scoped property: 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:
| Value | Meaning |
|---|---|
| 0 | Nothing is saved. Every property holds its documented default. |
| 1 | The most recently saved snapshot is in effect. |
| 2 | A 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. |
| 3 | A 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
- Values 2 and 3 persist for the remainder of the power cycle and
MUST NOT be cleared by
CMD_RSTorCMD_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 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:
| Octets | Field |
|---|---|
| 1 | Field flags |
| 0 or 2 | Battery voltage, UINT16_LE, millivolts |
| 0 or 1 | Battery 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:
| Value | Name |
|---|---|
| 0 | BATTERY_CHARGE_STATE_DISCHARGING |
| 1 | BATTERY_CHARGE_STATE_CHARGING |
| 2 | BATTERY_CHARGE_STATE_CHARGED |
BATTERY_CHARGE_STATE_DISCHARGING- The charging system reports neither active charging nor charge completion. This is the charge state used for a disconnected battery when the implementation can detect that condition; 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 charging system reports that charging has
completed. A battery at 100 percent while operating without external power
remains in
BATTERY_CHARGE_STATE_DISCHARGING.
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 overheard routable frames according to Repeater Operation, and it sets the repeater capability bit in its node identity. When false, the device identity does not forward 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, 64 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 16 B 16 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
| Type | Name | Value | Matches |
|---|---|---|---|
| 0 | FILTER_DEST_HINT | 3 octets | Frames whose destination hint field equals the value |
| 1 | FILTER_CHANNEL_ID | 2 octets | Channel-addressed frames (MCST, BUNI, BUAR) whose channel identifier equals the value |
| 2 | FILTER_PKT_TYPE | 1 octet | Frames 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_SETsupport 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 MIC 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 MIC while
its destination hint names the remote peer. The device therefore records the
leading 4 MIC bytes of each frame it transmits on the host’s behalf and
implicitly accepts any received frame whose trailer opens with a recorded
value — for a MAC Ack this matches the returning acknowledgement, and for
other packet types it matches the host’s own send being carried onward, which
the host’s forwarding-confirmation machinery must overhear 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.
Buffered-Frame Metadata
The Recv metadata of STR_PHY_RAW
(see Metadata for Recv) is
extended with two trailing fields:
RX_FLAGS(u8): Buffered-frame flagsRX_FLAG_BUFFEREDBit 0: The frame was held in the inbound queue and is being delivered byCMD_QUEUE_DRAIN.RX_FLAG_ACKEDBit 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.- 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. Live deliveries MAY therefore continue to 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:
PROP_HOST_AUTO_ACKis true and no host is attached.- The frame’s packet type requests acknowledgement:
UNAR, orBUARwhere the device also holds the frame’s channel key. - The frame is addressed to the host identity: its (possibly decrypted)
destination hint matches
PROP_HOST_KEY, and its source resolves to an entry inPROP_HOST_PEER_KEYS— by full public key when theSflag is set, or by unique 3-byte prefix match otherwise. - The frame authenticates: its MIC verifies under the pairwise
K_MICforUNAR, or under the combined blind unicast payload keys forBUAR. - 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.
- 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_GETand all device-emitted notifications report key-bearing properties without their secrets (see (#multi-value-properties)): peer public keys withoutK_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_SETandCMD_PROP_INSERTfor the key tables, and any set ofPROP_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_SAVEMUST 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):
| Id | Name |
|---|---|
| 19 | STATUS_ALREADY |
| 20 | STATUS_ITEM_NOT_FOUND |
STATUS_ALREADY- The requested state is already in effect; in particular, the item passed
to
CMD_PROP_INSERTis already present in the property. STATUS_ITEM_NOT_FOUND- The item or selector passed to
CMD_PROP_REMOVEdoes not match any item in the property.
Additional Reset Codes
The full protocol assigns one additional reset code (see Reset Codes):
| Id | Name |
|---|---|
| 115 | STATUS_RESET_RESTORED |
STATUS_RESET_RESTORED- Protocol reset into the saved snapshot, emitted when a device completes
CMD_RESTOREin 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. LikeSTATUS_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):
| Code | Name | Requires | Grants |
|---|---|---|---|
| 32 | CAP_HOST_FILTER | — | PROP_HOST_KEY, PROP_MAC_PROMISCUOUS, PROP_HOST_RX_FILTERS, and the receive-filtering behavior |
| 33 | CAP_HOST_RX_QUEUE | CAP_HOST_FILTER | The inbound queue, its properties, CMD_QUEUE_DRAIN, and the buffered-frame metadata |
| 34 | CAP_HOST_KEYS | CAP_HOST_FILTER | PROP_HOST_CHANNEL_KEYS and PROP_HOST_PEER_KEYS |
| 35 | CAP_HOST_AUTO_ACK | CAP_HOST_KEYS, CAP_HOST_RX_QUEUE | PROP_HOST_AUTO_ACK and acknowledgement delegation |
| 36 | CAP_SAVE | — | CMD_SAVE, CMD_RESTORE, PROP_SAVED, and boot-time restoration of saved state |
| 37 | CAP_DEV_IDENTITY | — | The device identity: PROP_DEV_KEY, PROP_DEV_PRIVATE_KEY, PROP_DEV_CHANNEL_KEYS, PROP_DEV_PEERS |
| 38 | CAP_DEV_NAME | — | PROP_DEV_NAME |
| 39 | CAP_BATTERY | — | Battery-powered operation and PROP_BATTERY |
| 40 | CAP_REPEATER | CAP_DEV_IDENTITY | PROP_MAC_REPEATER_ENABLED and autonomous repeater forwarding by the device identity |
| 41 | CAP_IDENT | CAP_DEV_IDENTITY | PROP_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.
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 6 | Magic | ASCII UMSHID |
| 6 | 1 | Format version | 0x01 |
| 7 | 1 | KDF identifier | 0x01 = Argon2id |
| 8 | 4 | Argon2id memory cost m | KiB |
| 12 | 4 | Argon2id time cost t | passes |
| 16 | 1 | Argon2id parallelism p | lanes |
| 17 | 16 | KDF salt | random per export |
| 33 | 1 | Cipher identifier | 0x01 = UMSH SIV construction |
| 34 | 16 | MIC | full AES-CMAC tag |
| 50 | — | Ciphertext | encrypted 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-V1"
okm = HKDF-SHA256(ikm, salt, info, 32)
K_enc = okm[0..15]
K_mic = okm[16..31]
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 SIV-style construction used for packets (see Encrypted Packets), with the full 16-byte MIC and the envelope header as associated data:
- Compute the full 16-byte AES-CMAC over the envelope header followed by
the payload plaintext, using
K_mic. - Use the MIC directly as the 16-byte CTR IV, as in standard AES-SIV.
- Encrypt the payload using AES-128-CTR with
K_encand that IV.
To import, derive the keys, decrypt the ciphertext, recompute the CMAC 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.
| Key | Section | Type | Presence |
|---|---|---|---|
| 1 | Identity | map | required |
| 2 | Counters | map | required |
| 3 | Channels | array of maps | optional |
| 4 | Contacts | array of maps | optional |
| 5 | Application settings | map | optional |
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
| Key | Field | Type | Presence |
|---|---|---|---|
| 1 | Private key seed | 32-byte byte string | required |
| 2 | Created | unsigned (Unix seconds) | optional |
| 3 | Display name | text string | optional |
| 4 | Advertisement | byte string | optional |
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
| Key | Field | Type | Presence |
|---|---|---|---|
| 1 | TX counter floor | unsigned | required |
| 2 | Restore generation | unsigned | required |
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:
| Key | Field | Type | Presence |
|---|---|---|---|
| 1 | Channel key | 32-byte byte string | required |
| 2 | Kind | unsigned | required |
| 3 | Name | text string | optional |
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:
| Key | Field | Type | Presence |
|---|---|---|---|
| 1 | Public key | 32-byte byte string | required |
| 2 | Local alias | text string | optional |
| 3 | Advertisement | byte string | optional |
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
-
Parse the envelope; reject unknown version, KDF, or cipher identifiers.
-
Derive keys from the passphrase and header parameters; decrypt and authenticate. Report authentication failure generically.
-
Derive the public key from the seed and present the identity (name, complete address) for explicit confirmation before committing anything.
-
On confirmation, compute the restored outbound frame counter:
block = 2^24 restored = (floor(tx_floor / block) + 2) * blockIf
restoreddoes 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. -
Persist the identity, the restored counter value, and the incremented restore generation before the identity sends any authenticated traffic.
-
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:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | Part magic | ASCII UMQR |
| 4 | 1 | Framing version | 0x01 |
| 5 | 4 | Artifact check | first 4 bytes of SHA-256 of the complete envelope |
| 9 | 1 | Part index | 0-based |
| 10 | 1 | Part count | total parts, ≥ 1 |
| 11 | — | Chunk | envelope 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 single virtual repeater whose radios sit in different places, connected by an authenticated tunnel over a reliable stream transport such as the internet. It specifies the tunnel wire protocol, the forwarding rules the bridge applies, and how the bridge satisfies the forwarding-confirmation expectations of the meshes it joins.
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.
- The server owns the bridge’s node identity. Trace routes crossing the bridge carry this identity’s router hint, source routes name it, and packets addressed to it are processed by the server whatever interface they arrive on.
- The server and each client front a ULCP device of their own, attached as a tethered host with promiscuous delivery enabled (see Radio Attachment).
- The server’s interfaces are its own radio plus one interface per connected client. All forwarding decisions are made at the server; clients relay frames between their radio and the tunnel and apply no forwarding logic of their own.
The whole assembly is one virtual repeater. It maintains a single duplicate-suppression cache shared across all interfaces, performs hop accounting once per crossing, and prepends its router hint to a trace route once per crossing — regardless of how many interfaces or clients participate.
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 each participant’s 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 client’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.
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: a candidate reception — the body of the
CMD_STR_RECVthat delivered the frame, written unmodified. The receive metadata SHOULD be present (sentinel-filled where the radio cannot measure), because the server’s signal-quality checks depend on it. - Server to client: a transmit request, passed unmodified as the body
of a
CMD_STR_SENDonSTR_PHY_RAW.
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 has already outlived every forwarding-confirmation retry that could have wanted it.
Radio Attachment
Each participant attaches to its device as an ordinary tethered host and
sets PROP_MAC_PROMISCUOUS to true.
The property is session-scoped and reverts on every attach, so it must be
re-asserted after each reconnection to the device. Because
promiscuous-only frames are never queued while the host is detached, a
device outage leaves no stale backlog to drain — attachment starts clean
by construction.
Transmission requires the device to advertise
CAP_WRITABLE_RAW_STREAM. Participants
SHOULD use confirmed transmissions (non-zero TID) so that duty-limit
and channel failures 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 confirmed transmission reports channel-access failure to the
participant rather than retrying on its own, and the local control
protocol carries no retry count to delegate one: one CMD_STR_SEND is
one channel-access attempt. The
backoff procedure is therefore
the participant’s to run, and it runs the same one every other
transmitter on that segment does. A participant MUST continue
draining its device’s receive path while it waits out a backoff; a
participant that stops listening in order to talk costs its segment
other stations’ traffic as well as its own.
The server provisions its host domain as any MAC-owning host would. Clients SHOULD NOT configure a host key or acknowledgement delegation on their devices; the bridge identity’s node logic lives entirely at the server.
Forwarding Procedure
The bridge follows the repeater forwarding procedure, adapted to multiple interfaces. For a frame arriving on interface I:
-
Duplicate suppression — Check the shared cache using the repeater cache-key rules. If the key is present, do not forward; see Re-confirmation for the one transmission a duplicate may still trigger. A Route Retry variant is a distinct cache key and forwards normally.
-
Local origin and local destination — If the source or destination address identifies the bridge’s own identity, do not forward. This is also what keeps the bridge’s own transmissions, overheard by another of its interfaces, from being re-bridged.
-
Locally handled unicast — If the frame was a unicast (blind or direct) fully handled by the server’s node logic, do not forward.
-
Unknown critical options — If the frame carries a critical option the bridge does not understand, do not forward.
-
Policy — Apply local bridge policy: per-interface and per-interface-pair forwarding rules, per-client rate limits, and region matching if configured. The bridge MUST NOT rewrite existing region codes and, unlike an ordinary repeater, SHOULD NOT insert one: its interfaces may sit in different regions, and a code added at one segment’s exit would misdescribe the packet everywhere else it travels.
-
Source-route match — If the frame carries a non-empty source-route option: if the next hint does not match the bridge’s router hint, do not forward; otherwise remove the hint, preserving the option even when it empties. This is a source-routed hop: skip steps 7 and 8.
-
Flood hop accounting — If the frame has a flood hop count with
FHOPS_REM > 0, decrementFHOPS_REMand incrementFHOPS_ACC. Otherwise, do not forward. -
Signal-quality thresholds — Apply minimum-RSSI and minimum-SNR checks against the measurements of the radio that heard the frame — for a client interface, the receive metadata carried alongside the tunneled frame.
-
Exit clamp — Clamp
FHOPS_REMto the configured exit maximum. The default is 1, and internet-tunneled deployments SHOULD NOT raise it. The clamp applies to source-routed hops as well, bounding the flood budget of a hybrid route that transitions to flooding beyond the bridge. -
Trace route — If the frame carries a trace-route option, prepend the bridge’s router hint; if that would exceed the maximum frame size, drop the frame.
-
Accept — Insert the cache key into the shared cache now, before any transmission.
-
Fan-out — Transmit the rewritten frame on every interface except I, subject to the policy decisions of step 5.
-
Confirmation copy — Transmit the confirmation copy on I, as specified in Forwarding Confirmation.
Fan-out transmissions introduce the packet to segments that have not yet carried it, so the flood forwarding contention window does not apply to them; ordinary channel access does.
Forwarding Confirmation
A bridge retransmits on the arrival interface so the previous hop can
confirm forwarding, as
Routing Overview § Bridging requires. The
confirmation copy is the rewritten frame exactly as fanned out in
step 12, except that FHOPS_REM is forced to zero (when the field is
present).
This form is deliberate:
- The previous hop still recognizes it. Confirmation matches on the cache key, which excludes the flood hop count for every packet type.
- It recruits no forwarders. A repeater receiving the copy rejects it at flood hop accounting, so the bridge is flood-neutral on the arrival segment: it confirms receipt without extending the local flood.
- It cannot sterilize the live flood. Repeaters insert cache keys only on
acceptance for forwarding; a repeater that hears the zero-
FHOPS_REMcopy before the live flood reaches it caches nothing and forwards the live copy normally. Bridges depend on that insertion timing, which is therefore normative for them. - Delivery from the copy is sound. A destination hearing only the
confirmation copy processes it normally, and its
FHOPS_ACChonestly counts the hop through the bridge.
The copy is a full-length frame, so this mechanism confirms cheaply in forwarding terms but not in airtime; the airtime optimization remains open (see Bridge Hop Confirmation).
For flood hops, the confirmation copy is a flood forward on a segment actively carrying the packet: it SHOULD use the contention window, defer when another forwarding of the same packet is overheard, and may be abandoned under the usual deferral rules — any overheard forwarding confirms the previous hop just as well. For source-routed hops the copy is the previous hop’s only confirmation; it uses ordinary channel access and is not abandoned.
Re-confirmation
If the confirmation copy is lost, the previous hop retries — and the retry is a duplicate the cache would otherwise suppress into silence, letting the previous hop exhaust its retry budget and wrongly declare the route failed. Therefore: when a duplicate of an accepted frame arrives on the interface it was originally accepted from, within a bounded window of the original acceptance, the bridge MAY re-transmit the confirmation copy on that interface. It MUST NOT re-forward the frame on any other interface. The considerations mirror the duplicate-acknowledgement window; thirty seconds is a reasonable default. Duplicates arriving on other interfaces are ordinary suppressed duplicates and trigger nothing.
This requires the cache entry to record the arrival interface and acceptance time alongside the key.
Acknowledgements Across a Bridge
The exit clamp makes flood-returned acknowledgements asymmetric. On the
forward path the clamp already limits delivery to nodes near the bridge’s
exit, so a destination that received the packet is bridge-adjacent. The
returning MAC ack, flooded with a
radius taken from FHOPS_ACC, is clamped again on its way back — and
dies there unless the originator is within the clamp of the bridge on
its own segment. Flood routing alone therefore does not round-trip an
acknowledgement across a bridge, and
route failure recovery
cannot repair this: 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-route option — the
bridge’s hint prepend is what makes the reversed trace routable — or be
sent along a known source route. A sender with neither should not expect
an acknowledgement back across a bridge.
Co-located Repeater Role
A device backing a bridge interface MAY additionally run its own repeater role, in which case two co-located repeaters exist — the device and the bridge — each with its own duplicate cache and hop accounting. This composes, with two rules:
- The device’s repeater role MUST NOT treat the tethered host identity’s hints as its own for source-route matching. Routed hops through the bridge identity belong to the bridge.
- The device’s normal flood re-forward on the arrival radio already serves as the previous hop’s confirmation, so the bridge SHOULD suppress its flood-hop confirmation copies on that interface. It MUST still emit confirmation copies for source-routed hops, which the device will not forward.
The division is imperfect: when the device’s own policy declines a forward, no flood confirmation is emitted by either party. This matches a standalone repeater declining the same packet, but deployments that want the bridge’s confirmation behavior to be exact SHOULD leave the repeater role disabled on bridge-backing devices.
Bridge-Originated Traffic
The bridge is a node present on every segment it touches. Its own traffic — application packets, beacons, and the MAC acks it generates as a destination — is ordinary node behavior applied per interface, not subject to the forwarding procedure or the exit clamp, transmitted on whichever interfaces its routing state selects. It MAY beacon on all interfaces. Step 2 of the forwarding procedure keeps this traffic from being re-bridged when one of its own transmissions is overheard through another interface.
Operational Guidance
- Rate limiting. The server SHOULD rate-limit forwarding per client and per interface pair. An authenticated but misbehaving client is the realistic failure mode, and the device duty ledger should be the backstop, not the policy.
- Co-located clients. Two clients whose radios share a segment cause every fanned-out frame to be transmitted twice there. The shared cache keeps their mutual receptions from looping, but the duplicate airtime is real; policy (step 5) is the place to group or exclude them.
- Region codes. A bridge whose segments sit in different regions should rely on region matching in step 5 rather than tagging: see the insertion prohibition there.
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 — duplicate suppression, the exit clamp, per-client rate limits, and duty-cycle enforcement — plus revocation of the client’s credential.
The bridge forwards frames it cannot decrypt; promiscuous delivery is what a repeater’s role requires, not an information grant. Operators should still treat the server as privileged infrastructure: it observes the metadata of every frame on every segment it bridges.
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-128-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 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).
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-inspired construction derives the CTR IV from the MIC, 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 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. Other channel members cannot cryptographically distinguish the true sender from an impersonator. 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 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. A broadcast packet is unauthenticated by design, and a multicast packet may be attributable only to a shared channel key or 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 trace-route option, 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. When the application layer includes an EdDSA signature in the payload (as required by the EMERGENCY channel), even this narrow property is lost — a signature can only be produced by the private key holder.
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. 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 Key Erasure.
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:
| Field | Packet types | What it reveals |
|---|---|---|
| Packet timing and frequency | All | Communication patterns — when a node is active, how often it transmits |
| Destination hint (3 bytes) | Unicast | Stable per-identity; enables tracking a node’s correspondents over time |
| Source hint (3 bytes) | Unicast, unencrypted multicast, broadcast | Stable per-identity; enables tracking a node’s activity over time |
| Channel identifier (2 bytes) | Multicast, blind unicast | Stable per-channel; reveals which channel a packet belongs to |
| Frame counter | All authenticated | Monotonically increasing; reveals total packet count and transmission rate |
| Flood hop count | All with FHOPS | Reveals approximate distance from the original sender |
| Packet size | All | May correlate with payload type or content length |
| MIC | All authenticated | Unique per-packet; usable as a packet fingerprint for correlation across hops |
| Ack MIC (4 bytes) | MAC Ack | Prefix 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 an AES-SIV-inspired construction 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. The AES-SIV construction degrades gracefully — nonce reuse reveals only whether two plaintexts are identical, without compromising keys or enabling forgery. See the FAQ.
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 an EdDSA payload signature. These requirements ensure that emergency traffic is universally readable and cryptographically attributable. 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.
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 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 that the claimed sender actually produced the packet. When the S flag is set, the source public key is carried in the packet and can be used for application-level trust decisions, but the protocol-level 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.
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 and trace-route addresses.
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 size | Collision 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-route 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-route options.
Open Issues
Bridge Hop Confirmation
A bridge is a node that relays UMSH packets over a different medium or channel than the one it received them on — for example, an internet backhaul, a wired link, or a different radio band connecting two geographically distant segments. Bridges are transparent to the protocol at the MAC layer: they consume source-route hints and forward packets exactly as repeaters do.
The simplest approach to bridge confirmation is to have the bridge retransmit the packet on the same inbound medium in addition to forwarding it to the other medium. This fully preserves the existing implicit confirmation mechanism with no protocol changes — the previous-hop sender hears the retransmission and confirms delivery exactly as it would with a normal repeater. The cost is doubled on-air time for every bridged packet on the inbound segment. Internet Bridging § Forwarding Confirmation specifies this retransmission with FHOPS_REM forced to zero, which confirms without recruiting additional forwarders; the airtime cost stands.
Retransmitting the entire packet solely to signal receipt is wasteful. However, if the bridge does not retransmit, the previous-hop node cannot observe the bridge’s onward transmission and will assume delivery failed — triggering retries that are even more wasteful, since the bridge did receive the packet successfully.
To be honest, it isn’t entirely clear that this is a problem worth optimizing. Bridges aren’t expected to exactly be common. But it is worth thinking about, so here is a possible solution:
Possibility: Hop Signal for non-retransmitting bridges
A Hop Signal is a local-only BCST (no FHOPS field, so it is never forwarded) emitted by the bridge on its inbound medium immediately after handling a packet. The BCST carries a MIC reference to the original packet for correlation, a signal type (Hop Ack or Hop Nak), and the bridge’s own 3-byte SRC hint for identification. It is smaller than a full retransmission and can also convey failure (Hop Nak) rather than just presence.
Because only one packet type slot (value 5) remains reserved, a Hop Signal would be defined as a MAC option on BCST rather than a dedicated packet type, preserving the reserved slot for a future use case with stronger architectural justification.
Hop signals would be informational only. A forged Hop Ack is equivalent to silent dropping — already in the threat model — so senders must still fall back to full MAC ack timeout if no Hop Ack arrives. The format of the Hop Signal option and the complete emission rules have not yet been defined.
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 trace route option, 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 trace route, there is no viable return path and no error feedback is possible.
Possible Approach: Trace-Route Return Path
If the original packet carries a trace route option, 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 route to signal willingness to receive error feedback; omit it to suppress errors. No special flag or option is needed. If no trace route 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 keyed 4-byte ack tag derived by encrypting the full 16-byte CMAC with K_enc. Only the ack tag authenticates the ack, and computing it requires the encryption 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 derive the keyed ack tag without K_enc. Blind forgery of the 4-byte tag succeeds with probability 2^-32 per attempt, which is infeasible to brute-force online over a bandwidth-limited LoRa channel; and even a successful forgery only causes a false delivery confirmation (a reliability denial-of-service), never a confidentiality or integrity 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-route 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-route option present. Repeaters prepend their router hints as they forward. The recipient can use the accumulated 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. 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. 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 CMAC 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 route, 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, and the AES-SIV-inspired construction provides nonce-misuse resistance as an additional safety margin.
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 an AES-SIV-inspired construction 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. The SIV-style construction used by UMSH is nonce-misuse-resistant: even if a nonce is accidentally reused, the only consequence is that an observer can detect that two plaintexts are identical. Confidentiality and authenticity are otherwise preserved. This robustness is worth the minor overhead of computing the MIC before encryption.
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-route option present. Repeaters prepend their router hint as they forward. When the peer receives the packet, the trace-route option 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.12.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.
| Aspect | UMSH | MeshCore |
|---|---|---|
| Identity key | 32-byte Ed25519 public key | 32-byte Ed25519 public key |
| Source address in packets | 3-byte hint (S=0), or full 32-byte key (S=1) | 1-byte hash (first byte of public key) |
| Destination address | 3-byte hint | 1-byte hash |
| Channel identifier | 2-byte derived hint | 1-byte hash of SHA-256 of channel key |
UMSH uses 3-byte hints for node addresses and 2-byte hints for router and trace-route addresses, 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 hashes for all regular addressing, with a dedicated ANON_REQ packet type that 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
| Aspect | UMSH | MeshCore |
|---|---|---|
| Header | 1-byte FCF with version, type, flags | 1-byte header with version, type, route mode |
| Packet types | 8 (via 3-bit field in FCF) | 16 payload types (via 4-bit field) |
| Routing info | CoAP-style options (source route, trace route, region, RSSI/SNR thresholds) | Path field (up to 64 bytes), transport codes |
| Flood hop count | Split 4-bit FHOPS field (max 15) | Implicit via path length |
| Region support | Optional region code option | Transport codes (2 × 16-bit) |
UMSH separates routing metadata into composable options, allowing packets to carry source routes, trace routes, signal-quality thresholds, and region codes independently. MeshCore uses a simpler flat structure with a path field and route-type bits.
Cryptography
| Aspect | UMSH | MeshCore |
|---|---|---|
| Encryption algorithm | AES-128-CTR (SIV-style: MIC used as CTR IV) | AES-128-ECB |
| Authentication | AES-CMAC (4/8/12/16-byte MIC) | HMAC-SHA256 (truncated to 2-byte MAC) |
| Key derivation | HKDF-SHA256 with domain-separated keys (K_enc, K_mic) | Raw ECDH shared secret used directly |
| Key separation | Separate 16-byte encryption and 16-byte MIC keys | Same shared secret for both AES key (first 16 bytes) and HMAC key (full 32 bytes) |
| Nonce misuse resistance | Yes (SIV construction) | N/A (ECB mode is deterministic) |
| Replay protection | 4-byte monotonic frame counter (timestamp-free) | Hash-based duplicate cache (128 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-128-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
MACThenDecryptfunction usesmemcmpto 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
| Aspect | UMSH | MeshCore |
|---|---|---|
| Flood routing | Yes, bounded by flood hop count | Yes (ROUTE_TYPE_FLOOD) |
| Direct/source routing | Yes, via source-route option | Yes (ROUTE_TYPE_DIRECT) |
| Hybrid routing | Source route + flood hop count in same packet | Not supported |
| Path discovery | Trace-route option on any flooded packet | Dedicated PATH payload type |
| Route learning | Trace-route option accumulates hints during flooding; reversed into source route by recipient | Explicit returned-path messages |
| Forwarding confirmation | Yes (retries with backoff) | Not defined |
| Channel access | CAD with random backoff; SNR-based contention windows | Listen-before-talk with random backoff; SNR-based flood retransmit delay |
| Signal-quality filtering | Min RSSI and min SNR options | SNR-based retransmit prioritization (implicit, no explicit thresholds) |
| Region-scoped flooding | Region code option | Transport 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.
Both protocols support automatic route learning, but through different mechanisms. UMSH uses a trace-route option that accumulates router hints as a packet floods; the recipient reverses the accumulated trace and caches it 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
| Aspect | UMSH | MeshCore |
|---|---|---|
| Multicast source concealment | Yes (source encrypted inside ciphertext when encryption enabled) | No |
| Blind unicast | Yes (source encrypted with channel key, payload with pairwise key) | No |
| Anonymous requests | Ephemeral Ed25519 key with S=1 flag | Dedicated ANON_REQ packet type |
| Metadata concealment | Channel-key-based, hides sender and/or destination from non-members | Not supported |
UMSH provides protocol-level privacy features that conceal sender and destination information from observers who do not possess the relevant channel key. Encrypted multicast conceals the source address, and blind unicast conceals both sender and destination. MeshCore does not define equivalent privacy modes.
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
| Aspect | UMSH | MeshCore |
|---|---|---|
| Channel key size | 32 bytes | Variable (shared secret) |
| Channel identifier | 2-byte derived hint | 1-byte hash of SHA-256 of key |
| Group message auth | Channel-key-based CMAC | Channel-key-based HMAC (2-byte MAC) |
| Sender authentication | Not cryptographically verified (symmetric key limitation) | Not cryptographically verified (same limitation) |
| Source privacy | Source encrypted when encryption enabled | No |
Both protocols share the fundamental limitation that symmetric-key multicast cannot authenticate individual senders — any channel member can forge a packet with any claimed source address.
Application Layer
| Aspect | UMSH | MeshCore |
|---|---|---|
| Payload typing | 1-byte payload type prefix | 4-bit payload type in header |
| Structured data | CoAP-over-UMSH (block-wise transfer) | Multipart packets |
| Node identity | Identity payload with role, capabilities, name, options, optional EdDSA signature | Advertisement payload with public key, timestamp, EdDSA signature, appdata |
| URI scheme | umsh:n:, umsh:ck:, umsh:cs:, coap-umsh:// | meshcore:// (contacts and channels) |
| Amateur radio | Operator/station callsign options, explicit unencrypted mode | Not 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
| Aspect | UMSH | MeshCore |
|---|---|---|
| Protocol scope | MAC layer with cleanly separated application protocols | Combined MAC, network, and application layer |
| Payload interpretation | Opaque at MAC layer — application protocols defined separately | Protocol defines payload types with application semantics (text messages, advertisements, login, etc.) |
| Fragmentation | Delegated to higher-layer protocols in the payload | Multipart packet type defined at protocol level |
| Node identity / advertisements | Application-layer payload (see Node Identity) | Protocol-level advertisement packet with mandatory fields |
| Time dependency | Timestamp-free — monotonic frame counters for replay protection (see Frame Counter) | Hash-based duplicate cache at MAC layer; relies on UNIX timestamps 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 relies on UNIX timestamps in several protocol-critical roles:
- Replay protection: MeshCore uses a fixed-size circular buffer of packet hashes (128 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 used to determine which advertisement is most recent.
- Login sequencing: The login handshake incorporates timestamps.
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 clock accuracy 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 128-entry buffer can wrap quickly, allowing replayed packets to be accepted after the original entry is evicted. MeshCore’s reliance on timestamps for advertisement freshness and login sequencing additionally requires nodes to maintain reasonably accurate clocks.
Packet Overhead Comparison
Minimum overhead for a typical encrypted unicast message (no options, no flood hop count):
| Field | UMSH (S=0, 16B MIC) | UMSH (S=0, 4B MIC) | UMSH (S=1) | MeshCore |
|---|---|---|---|---|
| Header/FCF | 1 | 1 | 1 | 1 |
| Path length | — | — | — | 1 |
| Destination | 3 | 3 | 3 | 1 |
| Source | 3 | 3 | 32 | 1 |
| Security info | 5 | 5 | 5 | — |
| MAC/MIC | 16 | 4 | 4–16 | 2 |
| ECB block padding | — | — | — | 0–15 (avg ~8) |
| Total overhead | 28 | 16 | 45–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 the transmitted MIC as the CTR IV) and then computing CMAC 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:
| Protocol | Destination hint width | False-positive rate per unicast packet |
|---|---|---|
| MeshCore | 8 bits (1 byte) | ~1 in 256 |
| UMSH | 24 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 any packet addressed to an unknown channel matches a channel you are not a member of. 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 are based solely on the flood hop count and duplicate suppression cache. 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. However, no packet is ever dropped due to poor signal quality; every repeater in range of a flooded packet will eventually retransmit it (subject to hop count), regardless of link quality. UMSH’s explicit signal-quality thresholds allow the sender to prevent retransmission over weak links entirely, avoiding wasted transmit power on paths unlikely to deliver the packet successfully.
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. This makes multi-hop forwarding much less reliable over long distances.
-
Nodes do not need accurate clocks. MeshCore uses UNIX timestamps for advertisement freshness and login sequencing, so nodes with drifted or reset clocks may reject valid advertisements or fail login handshakes. UMSH is timestamp-free at the MAC layer — no clock synchronization is required for any protocol operation.
-
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 128-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 Meshtastic firmware v2.5+ and its documentation and 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 cryptographic rigor, compact encoding, and clean layer separation. The comparison below highlights the technical differences without implying that one set of tradeoffs is universally better than the other.
Identity and Addressing
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Identity basis | 32-byte Ed25519 public key | 32-bit node number derived from Bluetooth MAC address |
| Cryptographic identity | Public key is the address | Optional Curve25519 keypair (PKC, v2.5+), not used for addressing |
| Source address in packets | compact 3-byte hint (S=0) or 32-byte key (S=1) | 4-byte node number (cleartext) |
| Destination address | 3-byte hint | 4-byte node number (cleartext) |
| Channel identifier | 2-byte derived hint | 1-byte DJB2 hash of channel name |
| Address spoofing resistance | Cryptographic — pairwise keys are derived from public keys | None — node numbers are hardware-derived and trivially spoofable |
UMSH identifies nodes by their Ed25519 public keys, which serve as both the address and the cryptographic credential. A node’s identity is inseparable from its ability to authenticate and decrypt. Meshtastic identifies nodes by a 32-bit number derived from the device’s Bluetooth MAC address. This number is not cryptographically bound to any key — any device can claim any node number.
Meshtastic added optional Curve25519 keypairs in v2.5 for direct message encryption, but these are not used for addressing. The node number remains the primary identifier, and channel-encrypted traffic has no per-node authentication regardless of whether PKC keys are configured.
Packet Structure
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Header | 1-byte FCF with version, type, flags | 16-byte fixed header (always cleartext) |
| Payload encoding | Raw bytes with 1-byte payload type prefix | Protobuf-encoded Data message |
| Packet types | 8 (via 3-bit field in FCF) | Implicit in protobuf portnum field (~30+ application types) |
| Routing info | CoAP-style composable options | Fixed fields: hop limit (3-bit), next hop (1 byte), relay node (1 byte) |
| Flood hop count | Split 4-bit FHOPS field (max 15) | Mandatory 3-bit field (max 7 hops) |
| Max LoRa payload | ~255 bytes | 255 bytes (233 bytes application payload after header and encoding overhead) |
| Typical unicast overhead | 14–28 bytes (depending on MIC size and source hint vs full key) | 16 bytes header + 28 bytes crypto = 44 bytes minimum |
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 and recipient node numbers, packet IDs, and channel hashes to any passive observer. UMSH’s addressing fields are compact hints that do not directly reveal node identity, and in blind unicast or encrypted multicast modes, the source address is encrypted.
Meshtastic encodes application payloads using Protocol Buffers (protobuf), which adds encoding overhead but provides a flexible, self-describing serialization format. UMSH uses raw byte payloads with a 1-byte type prefix, minimizing encoding overhead at the cost of less built-in structure.
Cryptography
| Aspect | UMSH | Meshtastic (channel) | Meshtastic (PKC DM) |
|---|---|---|---|
| Encryption | AES-128-CTR (SIV-style) | AES-128-CTR or AES-256-CTR, selected by PSK length; the default channel key is 16 bytes | AES-CCM |
| Authentication | AES-CMAC (4/8/12/16-byte MIC) | None | CCM auth tag |
| Key exchange | X25519 ECDH | Pre-shared key | Curve25519 ECDH |
| Key derivation | HKDF-SHA256 with domain separation | PSK used directly | SHA-256 of ECDH shared secret |
| Nonce construction | Frame counter + optional salt in SECINFO | Packet ID + sender node number | 8-byte random nonce |
| Replay protection | 4-byte monotonic frame counter | 32-bit random packet ID (duplicate cache) | Not defined |
| Forward secrecy | Optional PFS sessions | No | No |
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:
- An attacker who knows the channel key can modify ciphertext in transit (CTR mode bit-flipping), and the recipient has no way to detect the tampering.
- Any node with the channel key can forge packets claiming to be from any other node, since there is no per-node authentication and the sender’s node number in the cleartext header is not cryptographically bound to anything.
UMSH authenticates every secured packet with an AES-CMAC MIC (4–16 bytes, see MIC Size Selection Guidance). Even with a 4-byte MIC, UMSH provides 2^-32 forgery resistance — qualitatively different from Meshtastic’s complete absence of authentication on 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 UMSH provides on channel traffic 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.
PKC Direct Messages
Meshtastic v2.5+ added Curve25519 ECDH with AES-CCM for direct messages, providing both confidentiality and authentication. This is a substantial improvement over channel-only encryption, but applies only to direct messages — all broadcast traffic (position, telemetry, channel text) remains unauthenticated.
UMSH applies the same CMAC-based construction to unicast and multicast alike, so there is no secured traffic class left unauthenticated and no separate authenticated mode to opt into. Broadcast packets carry no security information at all and make no claim of authenticity.
Key Derivation
Meshtastic uses the channel PSK directly as the AES key for channel encryption, with no key derivation step. 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 is the standard practice recommended by cryptographic literature.
Routing
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Flood routing | Yes, bounded by flood hop count | Yes (managed flood with SNR-based priority) |
| Source routing | Yes, via source-route option | No |
| Hybrid routing | Source route + flood hop count in same packet | No |
| Next-hop routing | N/A | Yes (learned from ACK paths, v2.6+) |
| Max hops | 15 flood + “many” source-routed | 7 (3-bit hop limit) |
| Duplicate detection | MIC cache | Packet ID cache (32-bit random IDs) |
| Forwarding confirmation | Yes (hop-by-hop retries with backoff when source routing) | Implicit ACK for broadcasts (sender listens for neighbor rebroadcast, up to 3 retries); not hop-by-hop |
| Channel access | CAD with random backoff; SNR-based contention windows | SNR-based contention windows |
| Signal-quality filtering | Min RSSI and min SNR options | SNR-based rebroadcast priority (implicit) |
| Region-scoped flooding | Region code option | Not defined |
| Traceroute | Trace-route option on any packet | Dedicated TRACEROUTE_APP port |
Both protocols use flood-based routing as their primary delivery mechanism. Meshtastic’s managed flood uses SNR-based contention windows to prioritize better-positioned relays, which is an effective heuristic for reducing redundant rebroadcasts. UMSH provides explicit signal-quality thresholds (minimum RSSI and SNR options) that allow the sender to control relay eligibility per packet.
Meshtastic’s 3-bit hop limit caps multi-hop delivery at 7 hops. UMSH’s 4-bit flood hop count allows up to 15 flood hops, and source routing allows packets to traverse specific paths without flooding (with no hop limit).
Meshtastic v2.6+ added next-hop routing for direct messages: after a successful ACK exchange, the firmware learns which relay carried the response and uses it as a designated next hop for subsequent packets. UMSH achieves similar directed delivery through source-route options learned via trace routes — the recipient caches the accumulated trace directly as a source route for all subsequent communication with the sender, because the trace is already built most-recent hop first (see Route Learning).
Both protocols define channel access mechanisms. Meshtastic uses SNR-based contention windows to prioritize better-positioned relays. UMSH uses CAD (Channel Activity Detection) with random backoff and SNR-based contention windows for collision avoidance.
Both protocols provide forwarding confirmation, but with different scope. Meshtastic’s sender listens for any neighbor to rebroadcast a flooded packet; if no rebroadcast is overheard, the sender retransmits up to 3 times (with the final retry falling back to flooding if next-hop routing was in use). This provides 0-hop reliability — the originator can confirm that at least one neighbor forwarded the packet — but intermediate relays do not confirm onward delivery. UMSH defines hop-by-hop forwarding confirmation: each forwarding node (whether source-route hop or flood originator) listens for retransmission by the next hop and retries with backoff if none is heard, providing reliability across the full forwarding chain.
Privacy
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Header confidentiality | Addressing fields are compact hints; blind modes encrypt source/destination | Header always cleartext — sender, recipient, packet ID, channel hash exposed |
| Source concealment | Encrypted multicast, blind unicast | Not supported |
| Destination concealment | Blind unicast | Not supported |
| Node ID linkability | Public key (can use ephemeral keys) | Hardware MAC-derived (persistent identifier) |
| Anonymous first contact | Ephemeral Ed25519 key with S=1 flag | Not supported |
Meshtastic’s 16-byte cleartext header exposes the full sender and recipient node numbers on every packet. A passive observer with a LoRa receiver can identify who is communicating with whom, build traffic graphs, and track individual devices over time — even without the channel key. Node numbers are derived from hardware MAC addresses, making them persistent identifiers tied to physical devices.
UMSH’s compact hints reveal far less information to passive observers, and blind unicast and encrypted multicast modes encrypt the source and/or destination entirely. Nodes can also use ephemeral keypairs for anonymous communication.
Multicast and Group Communication
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Channel key size | 32 bytes | 1, 16, or 32 bytes (PSK) |
| Channel identifier | 2-byte derived hint | 1-byte DJB2 hash of channel name |
| Channels per node | Unlimited (implementation-defined) | Up to 8 |
| Multi-hop multicast | Yes (flood with flood hop count) | Yes (managed flood with hop limit) |
| Group message auth | Channel-key-based CMAC | None (AES-CTR only) |
| Source privacy | Source encrypted when encryption enabled | No (source in cleartext header) |
| Named channels | Yes (key derived from name) | Yes (name + PSK configured together) |
Both protocols support multiple channels with independent keys. Meshtastic limits nodes to 8 simultaneous channels. Meshtastic’s 1-byte channel hash has a high collision probability (1 in 256), requiring trial decryption when collisions occur. UMSH’s 2-byte channel identifier reduces this to 1 in 65536.
Application Layer
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Payload typing | 1-byte payload type prefix | Protobuf portnum field (~30+ types) |
| Payload encoding | Raw bytes | Protocol Buffers |
| Structured data | CoAP-over-UMSH (block-wise transfer) | Protobuf with defined message schemas |
| Text messaging | UMSH text message payload | TEXT_MESSAGE_APP (portnum 1) |
| Position/telemetry | Not defined (delegated to higher-layer protocols) | Built-in POSITION_APP, TELEMETRY_APP |
| Node identity | Identity payload with role, capabilities, name | NODEINFO_APP with User protobuf |
| Remote administration | Node management MAC commands | ADMIN_APP (portnum 6) |
| Audio | Not defined | AUDIO_APP (codec2, 2.4 GHz only) |
| Store and forward | Not defined | STORE_FORWARD_APP |
| Amateur radio | Operator/station callsign options, explicit unencrypted mode | is_licensed flag (lifts power limits, callsign via long name, manual PSK removal) |
| Implementation | Protocol spec (language-agnostic) | C++ firmware + protobuf definitions |
Meshtastic defines a rich application layer with built-in support for position sharing, telemetry, waypoints, audio, store-and-forward, and TAK integration. These are tightly integrated into the firmware and protobuf schema.
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 approach is less feature-complete out of the box but allows UMSH to carry arbitrary higher-layer content without protocol changes.
Both protocols address amateur radio operation, but at different levels. Meshtastic provides an is_licensed configuration flag that lifts firmware power limits and expects the operator to manually set their callsign as the node’s long name and clear the channel PSK to disable encryption. The callsign is carried in the existing user info field rather than a dedicated protocol field. UMSH defines amateur radio support at the protocol level: dedicated packet options carry operator and station callsigns as structured fields, and the Frame Control Field explicitly indicates whether a packet is encrypted. Both protocols require the operator to configure the device appropriately, but UMSH’s approach makes compliance structurally visible in the packets themselves — a monitoring station can verify callsign presence and unencrypted transmission by inspecting the packet, without needing to know the device’s configuration state.
Layer Separation
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Protocol scope | MAC layer with separate application protocols | Monolithic — radio, routing, and application layers interleaved |
| Payload interpretation | Opaque at MAC layer | Protobuf Data message decoded at every layer |
| Fragmentation | Delegated to higher-layer protocols | Not defined (single-frame limit) |
| Application coupling | Application protocols are architecturally separate | ~30+ application types defined in core protobuf schema |
UMSH maintains a clean boundary between the MAC layer and application protocols. The MAC layer treats payloads opaquely and can carry any higher-layer protocol. Meshtastic’s protobuf-defined Data message structure spans from radio-level fields to application payloads in a single schema, with no clean separation between layers.
Timestamps and Time Dependency
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Replay protection | 4-byte monotonic frame counter | 32-bit random packet ID (duplicate cache) |
| Timestamps in packets | None at MAC layer | rx_time metadata (not used for protocol decisions) |
| Clock synchronization required | No | No |
Neither protocol requires clock synchronization for core operation. Meshtastic includes reception timestamps as metadata but does not use them for routing or replay protection. Both protocols detect duplicates without relying on wall-clock time — UMSH via monotonic frame counters, Meshtastic via random packet ID caching.
Packet Overhead Comparison
Minimum overhead for a typical encrypted unicast message:
| Field | UMSH (S=0, 16B MIC) | UMSH (S=0, 4B MIC) | Meshtastic (channel) | Meshtastic (PKC DM) |
|---|---|---|---|---|
| Header/FCF | 1 | 1 | 16 | 16 |
| Destination | 3 | 3 | (in header) | (in header) |
| Source | 3 | 3 | (in header) | (in header) |
| SECINFO | 5 | 5 | — | — |
| MIC / auth tag | 16 | 4 | — | ~12 |
| Nonce / IV | — | — | (derived, not transmitted) | (8 in payload) |
| Protobuf overhead | — | — | ~6 | ~6 |
| Total overhead | 28 | 16 | ~22 | ~42 |
Meshtastic’s nonce is derived from header fields (packet ID + sender node number) rather than transmitted, saving bytes compared to protocols that transmit the IV. However, the fixed 16-byte cleartext header and protobuf encoding overhead partially offset this advantage.
UMSH with a 16-byte MIC has slightly more overhead than Meshtastic channel encryption (28 vs ~22 bytes), but UMSH’s overhead includes full authentication that Meshtastic’s channel mode lacks entirely. With a 4-byte MIC, UMSH achieves 16 bytes of overhead — lower than Meshtastic’s ~22 bytes — while still providing authentication that Meshtastic channel traffic does not have.
Meshtastic PKC direct messages add approximately 20 bytes of overhead beyond channel encryption (ECDH-derived key, CCM nonce, and authentication tag), bringing total overhead to roughly 42 bytes for authenticated direct messages.
Power Consumption
Power consumption on a battery-constrained LoRa node is driven by two factors: how long the radio is active (airtime), and how much CPU work is required after each received packet.
Channel Filtering and False Positives
In a LoRa mesh, broadcast and multicast traffic (position reports, telemetry, channel messages) outnumber unicast packets. For this traffic, the only pre-crypto filter available is the channel identifier. When a packet’s channel identifier matches a channel the node does not belong to, the node must attempt decryption to confirm the mismatch — a false positive.
| Protocol | Channel identifier width | False-positive rate per broadcast packet |
|---|---|---|
| Meshtastic | 8 bits (1-byte DJB2 hash) | ~1 in 256 |
| UMSH | 16 bits (2-byte derived hint) | ~1 in 65536 |
Meshtastic’s 1-byte channel hash produces ~256× more false positives than UMSH’s 2-byte channel identifier. Each false positive requires an AES-CTR decryption attempt (cheap relative to ECDH, but still unnecessary CPU work and a delay before the MCU can return to sleep). In a busy mesh with many active channels, this adds up.
Unicast Filtering
For unicast packets, Meshtastic’s 4-byte cleartext node number provides near-zero false-positive filtering with no cryptographic work — the destination can be checked by simple integer comparison before any decryption is attempted. UMSH’s 3-byte destination hint has a ~1-in-16,777,216 false-positive rate; when a collision occurs, verification requires decrypting the payload with AES-CTR and computing CMAC over the result — pairwise keys are cached after first contact so no ECDH is needed for known senders, but the decrypt-then-MAC work still applies.
This is a genuine power tradeoff: Meshtastic achieves cheaper unicast filtering by including the full destination identifier in the cleartext header, while UMSH accepts a small false-positive rate in exchange for transmitting fewer bytes and not fully exposing node identity to passive observers.
Packet Length and Airtime
Meshtastic’s fixed 16-byte header is transmitted on every packet regardless of content. UMSH’s header includes only the fields needed for the packet type. On a LoRa network, longer packets mean longer airtime, which means nearby nodes must keep their radios active longer to receive each packet — a cost that compounds across all nodes in range, not just the sender.
Repeater Power
Both protocols use flood routing as their primary delivery mechanism, so nodes configured as repeaters must receive and retransmit packets. Transmit is the most power-intensive radio operation on a LoRa node. In practice, repeating is enabled only on dedicated infrastructure nodes; end-user devices are typically configured as non-repeating and incur no forwarding transmit cost.
Meshtastic’s managed flood uses SNR-based contention windows: after receiving a packet, each potential relay waits a random delay inversely proportional to its received SNR before retransmitting. If it hears a better-positioned node retransmit first, it suppresses its own retransmission. This heuristic reduces the number of redundant rebroadcasts compared to simple flooding and saves transmit power across the network.
UMSH’s signal-quality filtering options (minimum RSSI and minimum SNR) allow the original sender to set explicit thresholds: a repeater that received the packet below the threshold must not retransmit it. This gives the sender direct control over which links are used for forwarding, avoiding transmit power wasted on paths unlikely to deliver the packet. Meshtastic’s SNR-based approach is automatic but implicit; UMSH’s approach is explicit but requires the sender to configure appropriate thresholds.
UMSH repeaters do not need to decrypt or verify the payload before forwarding — the MAC layer treats payloads opaquely. Meshtastic repeaters also forward without decryption for channel traffic.
Summary of Design Differences
Meshtastic is a full-featured, batteries-included mesh communication system with a large and active user community. It provides a rich application layer (position sharing, telemetry, store-and-forward, TAK integration), broad hardware support, and an easy on-ramp for non-technical users. Its channel-based encryption model is simple to configure and deploy.
UMSH prioritizes cryptographic robustness, compact encoding, and architectural cleanliness. It authenticates all traffic, provides privacy modes for metadata concealment, and maintains strict layer separation that allows it to carry arbitrary higher-layer protocols.
Key tradeoffs:
- Authentication: UMSH authenticates every packet. Meshtastic’s channel traffic has no authentication — only PKC direct messages (v2.5+) are authenticated.
- Privacy: UMSH provides compact addressing hints and opt-in blind modes. Meshtastic exposes full sender and recipient identifiers in cleartext headers on every packet.
- Identity model: UMSH uses cryptographic public keys as addresses. Meshtastic uses hardware-derived node numbers that are not cryptographically bound to any key.
- Overhead: Comparable in the common case (~14–26 bytes for UMSH vs ~22 bytes for Meshtastic channel), but UMSH’s overhead includes authentication.
- Application richness: Meshtastic provides a far richer built-in application layer. UMSH delegates richer functionality to higher-layer protocols.
- Layer separation: UMSH cleanly separates MAC and application concerns. Meshtastic is a monolithic system where protocol and application are interleaved.
- Implementation: Meshtastic is a mature C++ firmware with broad device support. UMSH is not tied to any implementation language or runtime, and its compact 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:
| File | Relevant claims |
|---|---|
RNS/Reticulum.py | MTU, header sizes, announce bandwidth cap, IFAC derivation |
RNS/Packet.py | Packet types, header layout, context values |
RNS/Identity.py | Key sizes, ECDH, HKDF derivation, ephemeral keys, ratchet system, announce format |
RNS/Destination.py | Destination types, destination hash construction, GROUP limitations, ratchet interval |
RNS/Transport.py | Routing, replay protection via packet hash cache, max hop cap, IFAC generation |
RNS/Cryptography/Token.py | AES-256-CBC, PKCS7, HMAC-SHA256, IV handling, timestamp removal |
RNS/Link.py | LINK session ECDH, symmetric key persistence, MTU signalling, link modes |
RNS/Interfaces/Interface.py | Link MTU auto-configuration (optimise_mtu) |
RNS/Discovery.py | On-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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Identity key | 32-byte Ed25519 public key | 64-byte keyset: 32-byte X25519 + 32-byte Ed25519 (Identity.py:58, KEYSIZE = 256*2) |
| Address in packets | compact 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 address | 3-byte hint (S=0) or full 32-byte key (S=1) | Not included (no source address in packets) |
| Destination address | 3-byte destination hint | 16-byte destination hash |
| Channel identifier | 2-byte derived hint | 16-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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Header | 1-byte FCF with version, type, flags | 2-byte header (flags + hop count) |
| Packet types | 8 (via 3-bit field in FCF) | 4: DATA, ANNOUNCE, LINKREQUEST, PROOF (Packet.py:60–63) |
| Destination types | Implicit in packet type (unicast, multicast, broadcast, blind) | 4: SINGLE, GROUP, PLAIN, LINK (Destination.py:63–66) |
| Routing info | CoAP-style composable options | Transport ID field (16 bytes) in HEADER_2 |
| Flood hop count | Split 4-bit FHOPS field (max 15) | Mandatory 1-byte field |
| MTU | LoRa 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, trace routes, 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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Encryption | AES-128-CTR (SIV-style: MIC used as CTR IV) | AES-256-CBC with PKCS7 padding (Token.py:91); AES-128 support removed in v1.0.0 |
| Authentication | AES-CMAC (16-byte MIC) | HMAC-SHA256 (32-byte tag, Token.py:50, TOKEN_OVERHEAD = 48) |
| Key exchange | X25519 ECDH | X25519 ECDH (Identity.py:581) |
| Key derivation | HKDF-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 handling | SIV construction (MIC as CTR IV) | Random 16-byte IV per packet (Token.py:89, os.urandom(16)) |
| Replay protection | 4-byte monotonic frame counter | Duplicate packet hash detection (Transport.py:59, packet_hashlist) |
| Per-packet overhead (crypto) | 5–7 bytes (SECINFO) + 16 bytes (MIC) = 21–23 bytes | 16 bytes (IV) + 32 bytes (HMAC) = 48 bytes minimum (LINK); +32 bytes ephemeral pubkey for SINGLE |
| Forward secrecy | Optional 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 an AES-SIV-inspired construction where the MIC doubles as 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 AES-CMAC 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:
| Component | UMSH (16B MIC) | UMSH (4B MIC) | Reticulum (SINGLE) | Reticulum (LINK) |
|---|---|---|---|---|
| SECINFO | 5 B | 5 B | — | — |
| MIC / HMAC | 16 B | 4 B | 32 B | 32 B |
| IV | — | — | 16 B | 16 B |
| Ephemeral pubkey | — | — | 32 B | — |
| CBC padding | — | — | 1–16 B (avg ~8) | 1–16 B (avg ~8) |
| Subtotal (crypto) | 21 B | 9 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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Flood routing | Yes, bounded by flood hop count | Yes (broadcast propagation) |
| Source routing | Yes, via source-route option | No |
| Hybrid routing | Source route + flood hop count in same packet | No |
| Transport/directed routing | N/A | Transport nodes with next-hop forwarding |
| Path discovery | Trace-route option on any flooded packet | Announce flooding + path request/response |
| Max hops | 15 flood + unlimited source-routed | 128 (hard-coded announce propagation cap, Transport.py:41, PATHFINDER_M) |
| Forwarding confirmation | Yes (retries with backoff) | No — forwarding is fire-and-forget; reliability is end-to-end via cryptographic proofs |
| Channel access | CAD with random backoff; SNR-based contention windows | Not defined at protocol level; RNode firmware implements CSMA with persistence probability for LoRa interfaces |
| Signal-quality filtering | Min RSSI and min SNR options | Not defined at protocol level |
| Region-scoped flooding | Region code option | Not defined |
| Announce bandwidth cap | Not defined (implementation policy) | Default 2% of interface bandwidth (Reticulum.py:115, ANNOUNCE_CAP = 2), configurable per-interface via announce_cap key |
| Interface discovery | Not defined | On-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-route option, which accumulates router hints as a packet floods — the recipient reverses the accumulated trace and caches it 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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Source address in packets | Yes (compact hint or 32-byte key) | No (default initiator anonymity) |
| Blind unicast | Yes (source encrypted with channel key) | N/A (no source to conceal) |
| Multicast source concealment | Yes (source encrypted inside ciphertext) | N/A |
| Anonymous first contact | Ephemeral Ed25519 key with S=1 flag | Per-packet ephemeral key for SINGLE destinations |
| Destination concealment | Not defined | Not defined |
| Interface access control | Not defined | IFAC (truncated Ed25519 signature per packet) |
| Network trust domains | Not defined | Network 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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Channel key size | 32 bytes | 32 bytes (AES-256) |
| Channel identifier | 2-byte derived hint | 16-byte destination hash |
| Multi-hop multicast | Yes (flood with flood hop count) | No (single-hop broadcast only) |
| Group message auth | Channel-key-based CMAC (16-byte MIC) | Channel-key-based HMAC-SHA256 (32-byte tag) |
| Source privacy | Source encrypted when encryption enabled | No source address to conceal |
| Named channels | Yes (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
| Aspect | UMSH | Reticulum |
|---|---|---|
| Payload typing | 1-byte payload type prefix | 1-byte context field (21 defined values; Packet.py:72–92) |
| Structured data | CoAP-over-UMSH (block-wise transfer) | Resources API (multi-packet reliable transfer) |
| Node identity | Identity payload with role, capabilities, name, options | Announce packets with public key, name hash, app data, Ed25519 signature (Identity.py:355, validate_announce()) |
| Service discovery | Beacon broadcasts | Aspect-based naming + announce propagation |
| Interface discovery | Not defined | On-network auto-discovery with trust verification (since v1.1.0) |
| Network identity | Not defined | Signing authority keypair for administrative domains (since v1.1.0) |
| Amateur radio | Operator/station callsign options, explicit unencrypted mode | Not defined |
| Implementations | Protocol spec (language-agnostic); experimental Rust reference implementation | Python 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.
| Aspect | UMSH | Reticulum |
|---|---|---|
| Replay protection | 4-byte monotonic frame counter | Duplicate packet hash cache |
| Timestamps in headers | None | None (explicitly removed from Fernet-derived token format; Token.py:41–49) |
| Clock synchronization required | No | No |
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 route). 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:
| Field | UMSH (S=0, 16B MIC) | UMSH (S=0, 4B MIC) | Reticulum (SINGLE) | Reticulum (LINK) |
|---|---|---|---|---|
| Header/FCF | 1 | 1 | 2 | 2 |
| Destination | 3 | 3 | 16 | 16 |
| Transport ID | — | — | — | 16 (if routed) |
| Source | 3 | 3 | — | — |
| Context byte | — | — | 1 | 1 |
| SECINFO | 5 | 5 | — | — |
| Ephemeral pubkey | — | — | 32 | — |
| IV | — | — | 16 | 16 |
| MIC / HMAC | 16 | 4 | 32 | 32 |
| CBC padding | — | — | 1–16 | 1–16 |
| Total overhead | 28 | 16 | ~108 | ~91 |
On a 255-byte LoRa frame:
| UMSH (S=0, 16B MIC) | UMSH (S=0, 4B MIC) | Reticulum (SINGLE) | Reticulum (LINK) | |
|---|---|---|---|---|
| Available payload | 227 B | 239 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 the transmitted MIC as the CTR IV) and then computing CMAC 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-route 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
- Source hint:
- 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
- Destination hint:
- 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 | RESERVED |
+---+-------+---+---------------+
Example 1: Broadcast Beacon (S=0)
A minimal beacon with a 3-byte source hint and no payload.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=0 (broadcast), S=0, R=0, H=0 | C0 |
| SRC | Node A hint | ED 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.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=0 (broadcast), S=1, R=0, H=0 | C4 |
| SRC | Node A full key | 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 |
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.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=2 (unicast), S=0, R=0, H=0 | D0 |
| DST | Node B hint | 6C 28 FD |
| SRC | Node A hint | ED 54 A5 |
| SCF | E=1, MIC=3 (16-byte), S=0 | E0 |
| Frame Counter | 42 | 00 00 00 2A |
| Payload | Encrypted 48 65 6C 6C 6F ("Hello") | 71 35 36 4B C1 |
| MIC | 16 bytes | 97 6D DC 92 2E BA 11 B7 2E 6B B1 7B 36 49 C5 4A |
D0 6C 28 FD ED 54 A5 E0 00 00 00 2A FF 71 35 36
4B C1 97 6D DC 92 2E BA 11 B7 2E 6B B1 7B 36 49
C5 4A
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.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=3 (unicast ack-req), S=1, R=0, H=0 | DC |
| DST | Node B hint | 6C 28 FD |
| SRC | Node A full key | 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 |
| SCF | E=1, MIC=3 (16-byte), S=0 | E0 |
| Frame Counter | 1 | 00 00 00 01 |
| Payload | Encrypted 68 65 79 ("hey") | 9C 77 59 |
| MIC | 16 bytes | E9 9F 4C 5F 9D 3E 4F 4E D3 CC B2 1E F5 C0 01 97 |
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 9C 77 59 E9 9F 4C
5F 9D 3E 4F 4E D3 CC B2 1E F5 C0 01 97
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.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=4 (multicast), S=0, R=0, H=0 | E0 |
| CHANNEL | Derived channel identifier | B0 8D |
| SCF | E=1, MIC=3 (16-byte), S=0 | E0 |
| Frame Counter | 5 | 00 00 00 05 |
| Encrypted data | ENCRYPT(`SRC | |
| MIC | 16 bytes | 30 E3 26 92 83 DB 9A 69 AB 12 64 1E B3 22 42 D6 |
E0 B0 8D E0 00 00 00 05 FF 39 E5 95 FE 97 AF A8
90 30 E3 26 92 83 DB 9A 69 AB 12 64 1E B3 22 42
D6
Total: 33 bytes.
Example 6: Authenticated Multicast (E=0)
An authenticated but unencrypted multicast from Node A carrying payload type 03 followed by "Hello".
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=4 (multicast), S=0, R=0, H=0 | E0 |
| CHANNEL | Derived channel identifier | B0 8D |
| SCF | E=0, MIC=3 (16-byte), S=0 | 60 |
| Frame Counter | 3 | 00 00 00 03 |
| SRC | Node A hint | ED 54 A5 |
| Payload | `03 | |
| MIC | 16 bytes | 53 A5 E2 91 F5 40 0A B9 87 FE C7 14 9D F8 97 24 |
E0 B0 8D 60 00 00 00 03 FF ED 54 A5 03 48 65 6C
6C 6F 53 A5 E2 91 F5 40 0A B9 87 FE C7 14 9D F8
97 24
Total: 34 bytes.
Example 7: Encrypted Unicast with Options and Flood Hops
An encrypted unicast with a region code option, an empty trace-route option, and flood hop limit 4.
Options encoding:
| Option | Number | Delta | Length | Encoding |
|---|---|---|---|---|
| Trace Route | 2 | 2 | 0 | 20 |
| Region Code | 11 | 9 | 2 | 92 then value 78 53 |
| End marker | — | — | — | FF |
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=2 (unicast), S=0, R=0, H=1 | D1 |
| FHOPS | FHOPS_REM=4, FHOPS_ACC=0 | 40 |
| DST | Node B hint | 6C 28 FD |
| SRC | Node A hint | ED 54 A5 |
| SCF | E=1, MIC=3 (16-byte), S=0 | E0 |
| Frame Counter | 10 | 00 00 00 0A |
| Options | Trace route + region code + end marker | 20 92 78 53 FF |
| Payload | Encrypted 68 65 79 ("hey") | F4 CF 71 |
| MIC | 16 bytes | C4 91 19 48 E1 C8 F1 32 05 6A 16 B1 06 34 74 D5 |
D1 40 6C 28 FD ED 54 A5 E0 00 00 00 0A 20 92 78
53 FF F4 CF 71 C4 91 19 48 E1 C8 F1 32 05 6A 16
B1 06 34 74 D5
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.
| Field | Value | Hex |
|---|---|---|
| FCF | VER=3, TYPE=6 (blind unicast), S=0, R=0, H=0 | F0 |
| CHANNEL | Derived channel identifier | B0 8D |
| SCF | E=1, MIC=3 (16-byte), S=0 | E0 |
| Frame Counter | 7 | 00 00 00 07 |
| ENC_DST_SRC | ENCRYPT(`DST | |
| ENC_PAYLOAD | ENCRYPT("Hello") | 4E 55 F2 08 51 |
| MIC | 16 bytes | F6 21 C9 8C 78 F7 90 92 34 0D E7 12 AA 07 AE 77 |
F0 B0 8D E0 00 00 00 07 FF A4 FB D3 6A A0 87 4E
55 F2 08 51 F6 21 C9 8C 78 F7 90 92 34 0D E7 12
AA 07 AE 77
Total: 36 bytes.