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-256: AES-CMAC-based S2V for message authentication and AES-CTR for payload encryption.
- AES-SIV (Synthetic Initialization Vector)
- A misuse-resistant authenticated encryption scheme defined by RFC 5297. It uses S2V to compute a synthetic IV from the associated data and the plaintext, then encrypts with AES-CTR. UMSH secures packets with AES-SIV using AES-256, extended with MIC truncation and a SECINFO-padded CTR IV; with a full 16-byte MIC the construction is byte-for-byte AEAD_AES_SIV_CMAC_512. See Encrypted Packets.
- ARNCE/HAM-64
- A compact character encoding scheme for amateur radio callsigns, encoding up to 12 characters into 2, 4, 6, or 8 bytes. Used in UMSH’s Operator Callsign and Station Callsign packet options.
- Beacon
- A broadcast or multicast packet with an empty payload. Beacons advertise a node’s presence and are used for path discovery and route learning. See Beacons & Path Discovery.
- Blind Unicast
- A packet type that carries a unicast payload addressed to a specific destination while concealing both the sender and destination identities from observers who do not possess the channel key. The destination hint and source address are encrypted using the channel key; the payload is protected end-to-end using keys derived from both the channel key and the pairwise shared secret. See Frame Types.
- Bridge
- An arrangement that carries UMSH packets between two different media or channels—for example, from a local LoRa radio to an internet backhaul and back to a distant LoRa radio. Bridges are protocol-transparent: a repeater at each end forwards the packet as it would any other, so a crossing consumes source-route hints, spends two flood hops, and records both repeaters in a trace. The inbound repeater’s forwarding is observable on the medium the packet arrived from.
- Broadcast
- A packet intended for all nodes in range. Broadcast packets carry no destination hint and are not encrypted or authenticated at the MAC layer. See Frame Types.
- CAD (Channel Activity Detection)
- A LoRa physical-layer feature that listens briefly for preamble energy on the channel with minimal power draw. UMSH uses CAD to implement listen-before-talk channel access, reducing collisions without requiring continuous reception. See Channel Access.
- Channel
- A logical communication group secured by a shared symmetric key. Channels serve two purposes: group communication (multicast) and metadata concealment (blind unicast). See Channels.
- CoAP (Constrained Application Protocol)
- A lightweight request/response protocol designed for constrained networks (RFC 7252). UMSH borrows CoAP’s delta-length option encoding for packet options and application-layer payloads.
- Confidentiality
- The property that payload content is accessible only to intended recipients. UMSH provides confidentiality via AES-256 encryption keyed with material derived from ECDH (unicast) or the channel key (multicast).
- Duplicate Suppression
- A mechanism by which repeaters track recently forwarded packets and decline to forward the same packet a second time. Each packet is identified by its MIC (for authenticated packets) or a locally computed hash (for unauthenticated packets). This prevents flood-routed packets from circulating indefinitely. See Repeater Operation.
- ECDH (Elliptic Curve Diffie-Hellman)
- A key agreement protocol that allows two parties to derive a shared secret using only their public keys and their own private keys. UMSH uses X25519 ECDH to derive pairwise shared secrets for unicast encryption and authentication.
- Ed25519
- An elliptic curve digital signature algorithm using the Edwards25519 curve. UMSH uses Ed25519 keypairs as node identities. The same keypair is converted to X25519 form for key agreement.
- EdDSA (Edwards-curve Digital Signature Algorithm)
- The family of signature algorithms that includes Ed25519. Used in UMSH for 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 Hop
- A hop spent from
FHOPS_REM: a transmission made withFHOPS_REM > 0and no source-route hint left to follow, which is what permits the repeaters that hear it to flood-forward the packet. The final transmission of a flood-routed packet is made withFHOPS_REM = 0and is a hop but not a flood hop, and a source-routed hop is not a flood hop either. A path’s hop count is its source-routed hops plus its flood hops plus one, so it is one more thanFHOPS_ACCif and only if no source route was involved. See Flood Hop Count. - Flood Routing
- A routing strategy where a packet is forwarded by every eligible repeater within the flood radius, subject to duplicate suppression. Requires no topology state at repeaters. Bounded by the flood hop count field (
FHOPS). See Repeater Operation. - Frame
- The unit of transmission at the LoRa PHY layer. A UMSH packet must fit within a single frame. The terms frame and packet are used interchangeably in this specification; frame emphasizes the physical transmission unit, packet emphasizes the logical protocol unit.
- Frame Counter
- A monotonically increasing 4-byte value included in every authenticated packet. The receiver tracks recently seen counter values and rejects packets with counters it has already processed, providing replay protection without requiring synchronized clocks. The counter must be persisted across reboots to prevent reuse. See Security & Cryptography.
- Hop
- One leg of a packet’s path through the network—the transmission from one node to an adjacent node within radio range. A packet that travels through two repeaters before reaching its destination has traversed three hops. A zero-hop message is a message sent to yourself that is never sent over the radio.
A hop taken by a repeater matching a source-route hint is a source-routed hop and spends no
FHOPSbudget; a hop spent fromFHOPS_REMis a flood hop. Between them the two counts leave exactly one transmission uncounted, so a path always has one more hop than the two counts together. - HKDF (HMAC-based Key Derivation Function)
- A key derivation function standardized in RFC 5869, composed of two steps: Extract (combining a secret and optional salt into a pseudorandom key) and Expand (stretching that key to the required output length). UMSH uses HKDF-SHA256 with domain-separated labels to derive encryption keys, authentication keys, and channel identifiers from shared secrets and channel keys.
- IoT (Internet of Things)
- A broad term for networked embedded devices. UMSH is designed to be applicable to IoT use cases in addition to human communication, though it is optimized for the latter.
- Latency
- The time elapsed between a packet being transmitted and its receipt or acknowledgement. LoRa’s low data rate and mesh forwarding introduce significant latency compared to IP networks.
- LoRa (Long Range)
- A proprietary wireless modulation technology using chirp spread spectrum, designed for long range and low power consumption at the cost of low data rate. LoRa payloads are typically limited to 200–250 bytes, and transmitting a single packet may take hundreds of milliseconds depending on spreading factor and bandwidth. UMSH is designed around these constraints.
- Long-Term Identity
- A node’s stable Ed25519 keypair, used as its persistent network identity. Contrast with ephemeral keypairs used in PFS sessions, which are discarded after use.
- MAC Layer (Medium Access Control)
- The sublayer of the data link layer responsible for addressing, packet framing, and channel access. In UMSH, the MAC layer handles packet types, addressing, routing options, and cryptography. Application-layer protocols are carried in the payload and are defined separately.
- MAC Ack
- A packet type generated by the final destination of a unicast packet (UNAR or BUAR) to confirm receipt. The MAC Ack carries an ack MIC—the first 4 bytes of the original packet’s MIC, a public handle that lets the sender and any forwarding repeater correlate the ack with the original packet—and a 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, the truncated S2V output (RFC 5297) computed over the packet’s static fields and payload. Size is configurable from 4 to 16 bytes. The term MIC is used instead of the more common “MAC” (Message Authentication Code) to avoid confusion with Medium Access Control. See Security & Cryptography.
- Multicast
- A packet intended for all members of a channel, identified by a 2-byte channel identifier derived from the channel key. See Frame Types.
- Node
- A logical participant in a UMSH network, defined by a unique Ed25519 keypair. A single physical device may host multiple nodes. A node may act as an endpoint (sending and receiving application data), a repeater (forwarding packets), or both.
- Packet
- The logical unit of the UMSH protocol. Every packet must fit within a single LoRa frame. See Frame above.
- Pairwise Key
- A symmetric key derived from X25519 ECDH between a specific sender and recipient. Each pair of nodes shares a unique set of pairwise keys (one for encryption, one for authentication) derived deterministically from their long-term key material. Pairwise keys are stable across sessions unless a PFS session is used. See Security & Cryptography.
- PFS (Perfect Forward Secrecy)
- A property ensuring that compromise of long-term keys does not allow retroactive decryption of past traffic. UMSH provides optional PFS via ephemeral keypair sessions. See Security & Cryptography.
- PHY (Physical Layer)
- The lowest layer of a network stack, responsible for modulation and transmission over the physical medium. In UMSH deployments, this is typically LoRa. The PHY layer is below the MAC layer and is not defined by this specification.
- Private Key
- The secret half of an asymmetric keypair. In UMSH, a node’s Ed25519 private key is used to derive its X25519 private key for ECDH and to sign payloads when required. Must never be transmitted or disclosed.
- Privacy
- Broader than confidentiality: the protection of metadata and communication patterns in addition to payload content. UMSH provides confidentiality but does not fully protect against traffic analysis. See Security Considerations.
- Public Key
- The public half of an asymmetric keypair, which can be freely shared. In UMSH, a node’s Ed25519 public key is its address. Address hints are derived from it.
- RSSI (Received Signal Strength Indicator)
- A measurement of received radio signal power, expressed in dBm (negative values; higher is stronger).
- Repeater
- A node that forwards packets to extend the effective range of the mesh. Repeaters participate in flood routing and source routing but do not generate or consume application payloads for the packets they forward. See Repeater Operation.
- SNR (Signal-to-Noise Ratio)
- A measurement of the ratio between received signal power and background noise, expressed in dB. Unlike RSSI, SNR remains a reliable indicator of link quality even when the signal is below the noise floor, which is common in LoRa’s spread-spectrum operating regime.
- Source Routing
- A routing strategy where the sender specifies the explicit sequence of repeaters a packet must traverse. Requires the sender to have prior knowledge of a valid path. See Packet Options.
- Trace 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)—short codes of one to three letters or digits encoded as 2-byte ARNCE values (e.g. SJC →
0x7853)
UTF-8 Strings
All human-readable text in UMSH—message bodies, node names, sender handles, room descriptions—is encoded as UTF-8. String length is determined by context:
- Inside a CoAP-style option, the option’s length field defines the string boundary.
- As trailing data after a
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 flood hops, which is a path of sixteen hops when no source route is involved; longer paths can be achieved by combining source routing with flooding (see Routing Implications).
FHOPS_ACC enables the destination to determine how many flood hops the packet traversed—one less than the hops it took, unless a source route carried it part of the way—which is used for MAC ack routing when no 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_identifier = HKDF-SHA256(channel_key, salt="UMSH-CHAN-ID", info="", L=16)
channel_id = first_2_bytes( channel_identifier )
The CHANNEL field carries channel_id. The full 16-byte channel identifier is what names a channel where two channels must be told apart without revealing either key—two keys can derive the same 2-byte channel_id, and management interfaces cannot leave that ambiguous.
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 an IV derived from the MIC (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+ 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).
A hop that measured neither field—one whose radio reports no signal quality, or one that received the packet over a point-to-point link rather than the air—prepends two zero bytes. An RSSI byte of zero is 0 dBm at the receiver, which no link this protocol runs over produces, so it cannot be read as a measurement. Such a hop still prepends an entry: the one-to-one pairing with the Trace Route option is what makes any entry in the list attributable, and a hop that skipped its own would misattribute every entry before it.
Region Code (option 11)
- Type: 2-byte region identifier
- Semantics: restricts flood-routing to repeaters configured for the specified region.
- A repeater configured for one or more regions MUST NOT flood-forward a packet whose region is none of them. A repeater configured for no regions makes no regional claim and applies no such restriction.
- This option MUST NOT be enforced on a source-routed hop, which includes the hop that removes the last remaining hint. A repeater named in the route forwards regardless of region.
- Multiple region-code options may appear on the same packet. In that case, a repeater MAY flood-forward the packet if any one of the listed regions matches local policy.
- Because this option is dynamic, repeaters may insert it while flood-forwarding a packet that currently has no region code.
- A repeater must never rewrite an existing region code and must never add a second region code to a packet that already has one or more region-code options.
- Region insertion is a local policy decision. When no explicit local policy exists, a reasonable default is the IATA code of the closest regional commercial airport.
- Region insertion applies only during flood forwarding. An untagged source-routed packet is first tagged by the repeater that flood-forwards it—the one that receives it with an already-empty source route—not by the repeater that emptied the route.
Region Code Encoding
Region codes are 2-byte identifiers derived by one of two methods, depending on the type of region. Both ignore ASCII case—ARNCE/HAM-16 encodes A–Z and a–z alike, and the hash folds A–Z to lowercase a–z before it is taken, altering no other character. Strings that differ only in ASCII case therefore derive the same code and name the same region.
As with channel names, folding is restricted to ASCII: correct case-folding of the full Unicode range is locale-dependent, and a name that folded under one implementation and not another would be two regions on one mesh.
Short codes. A region named by one to three ASCII letters or digits encodes directly into a 16-bit value with ARNCE/HAM-16, the unused trailing characters left as NUL. Three letters are conventionally the IATA code of the nearest airport or of a metro area that has one; two are conventionally an ISO 3166-1 alpha-2 country code or a bare subdivision code, for a region that spans one. Examples:
| Short Code | Region Code |
|---|---|
| SJC | 0x7853 |
| MFR | 0x5242 |
| US | 0x8638 |
| WA | 0x8FE8 |
An all-letter short code is exclusive: the transform below vacates every one of them, so no named region can ever derive one. A short code bearing a digit—W7, 5X2—encodes just as faithfully and no two short codes ever share a code, but it is not vacated, so it shares its space with the hashes. Such a code is worth using when a mnemonic is wanted in place of a hash, and it is nothing more than that: an implementation MUST NOT display a region code as a digit-bearing short code, because the code it is reading may equally have come from a name.
/, -, and ^, though ARNCE spells them, MUST NOT appear in a short code. The transformed codes are led by / and -, and a short code able to reach that space would read back as a region it is not.
Named regions. For regions that a short code does not suit (super-regions, cities without a nearby airport, geographic areas, etc.), the region code is the first two bytes of the SHA-256 hash of the folded region name (UTF-8 encoded), EXCEPT when performing ARNCE/HAM-16 decoding on the resulting value would yield nothing but letters—one, two, or three of them. In that case, you additionally perform the following transform:
def transform_letter_chunk(encoded: int) -> int:
"""Transform an all-letter ARNCE chunk into a non-letter ARNCE chunk."""
LETTER_MIN = 1
LETTER_MAX = 26
TRANSFORM_BASE = 27 * 1600 # 0xA8C0
TWO_LETTER_BASE = TRANSFORM_BASE + 26 ** 3 # 0xED68
ONE_LETTER_BASE = TWO_LETTER_BASE + 26 ** 2 # 0xF00C
a = encoded // 1600
b = (encoded // 40) % 40
c = encoded % 40
def is_letter(x: int) -> bool:
return LETTER_MIN <= x <= LETTER_MAX
if not is_letter(a):
return encoded
if is_letter(b) and is_letter(c):
return TRANSFORM_BASE + (a - 1) * 26 * 26 + (b - 1) * 26 + (c - 1)
if is_letter(b) and c == 0:
return TWO_LETTER_BASE + (a - 1) * 26 + (b - 1)
if b == 0 and c == 0:
return ONE_LETTER_BASE + (a - 1)
return encoded
The three lengths occupy three consecutive blocks, longest first, and together vacate 18,278 of the 64,000 encodable chunks. The highest code the transform yields is 0xF025; every one of them decodes to a string led by / or -, and so to nothing that reads as a region.
Examples, with the folded name each hash is taken over:
| Region Name | Folded | SHA-256 prefix | Region Code |
|---|---|---|---|
| Willamette Valley | willamette valley | 0xb02d... | 0xB02D |
| East Bay | east bay | 0x36e2... | 0x36E2 |
| Rogue Valley | rogue valley | 0x3f56... | 0xC0F9 |
| Wasatch Front | wasatch front | 0x5fa0... | 0xEEDF |
Note that the first two bytes of the SHA256 of “rogue valley” is 0x3F56, which would decode to JEN, so it is transformed to 0xC0F9, which decodes to no letters. “wasatch front” hashes to 0x5FA0, which would decode to the two letters OL, and is transformed to 0xEEDF for the same reason.
Thus, a hash-derived region code will never collide with an all-letter short code. This allows all region codes which decode to nothing but letters to be assumed to be short codes and to be used/displayed unambiguously without additional context.
However, collisions can still happen between hash-originated region codes, and between one of those and a digit-bearing short code. These collisions are rarely of practical concern. If a region code in one part of the world collides with a region code in a different part of the world, there is no actual ambiguity because flood repeating is an inherently local event. In the rare case of a collision within a geographic area, it can be resolved by adjusting the named region slightly (for example, making it more specific).
The assignment and scope of hash-derived region codes—and resolution of any collisions—are generally handled locally.
Literal codes. In any context that takes a region’s string form, a string of 0x followed by four hexadecimal digits (e.g. 0x31d9) denotes the two-byte code it spells. Strings of this shape are by definition excluded from the other two derivations—there is no named region 0x31d9, only the code. This is also the display form for a code that reads as no short code and whose name is unknown, so a region list read off the air can be fed back in unchanged. Note that a string of one to three characters is a short code before it is anything else: 0x1 encodes as the three characters 0, x, 1, and only 0x followed by four hexadecimal digits is a literal.
A region name MUST NOT exceed 24 bytes of UTF-8. Names travel on the wire in identity payloads, and the bound is what lets a list of them fit there.
Capitalization. Folding applies to derivation and comparison, not to storage: wherever a region is held or carried in its string form—a repeater’s configured list, the Supported Regions identity option—it keeps the capitalization it was written with. A set of regions holds at most one entry per folded string; writing a region already present in a different capitalization respells the entry rather than adding a second one. A short code is displayed uppercase whatever case it was written in.
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 AES-SIV (RFC 5297) with AES-256. Unicast packets are secured with pairwise keys derived from X25519 ECDH between sender and recipient Ed25519 keys. Multicast packets are secured with keys derived from the shared channel key. In both cases, a monotonic frame counter provides replay protection without requiring synchronized clocks.
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 synthetic IV V with S2V (RFC 5297 §2.4) and truncating to the specified length. The S2V output is an AES-CMAC, and truncation of CMAC outputs is permitted by NIST SP 800-38B.
MIC Size Selection Guidance
Shorter MICs save bytes on the wire but reduce forgery resistance and increase the probability of duplicate-suppression collisions in repeater caches (see Duplicate Suppression). The following guidelines help choose an appropriate MIC size:
-
16 bytes (default): Recommended for long-term stable identities where the same pairwise keys may be used for months or years. The cost of a successful forgery is high (attacker gains persistent access to impersonate a node), and the 2^-128 forgery probability makes brute-force infeasible regardless of how many packets an attacker can attempt.
-
8 bytes: A reasonable middle ground for most communication. Provides 2^-64 forgery probability—well beyond practical brute-force for LoRa’s low packet rates—while saving 8 bytes per packet. Suitable for general unicast and multicast traffic.
-
4 bytes: Appropriate for short-lived contexts where the keys will be discarded soon, such as PFS sessions or one-time exchanges using ephemeral node addresses. The 2^-32 forgery probability (~1 in 4 billion) is adequate when the window of exposure is brief. Also useful for latency-sensitive or payload-constrained scenarios where every byte matters, such as sensor telemetry on slow LoRa links.
-
12 bytes: Available as an intermediate option when 8 bytes feels too tight but 16 bytes is more overhead than warranted. Provides 2^-96 forgery probability.
As a general principle: the longer the keys will be in use and the higher the value of the traffic they protect, the larger the MIC should be. For ephemeral keys that will be erased within minutes, a small MIC is sufficient. For a node’s long-term identity keys, prefer the full 16 bytes.
Frame Counter
The 4-byte frame counter must increase monotonically 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.
Eligibility is not obligation: re-acknowledgement SHOULD be paced. Flood routing delivers one transmission as several copies, and a copy of a transmission whose acknowledgement was already sent proves nothing was lost. A receiver SHOULD NOT acknowledge the same packet more than once per forwarding-confirmation window, measured from its most recent acknowledgement of that packet: copies arriving inside the window share the acknowledgement already sent, while a sender that lost its acknowledgement cannot retransmit before its own confirmation window has lapsed.
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-V2"
okm = HKDF-SHA256(ikm, salt, info, 64)
The output keying material is split as follows:
K_mic = okm[0..32]
K_enc = okm[32..64]
Where:
K_micis the 32-byte message authentication key (the S2V key)K_encis the 32-byte encryption key (the CTR key)
This layout makes okm exactly the AES-SIV key K from RFC 5297: K_mic is the leftmost half K1 (S2V) and K_enc is the rightmost half K2 (CTR), so okm can be handed directly to an AEAD_AES_SIV_CMAC_512 implementation.
These keys are stable for the sender/recipient pair and may be cached by the implementation. They do not change from packet to packet.
Because the key derivation depends only on the ECDH shared secret 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 32-byte XOR per key, with no additional HKDF calls.
Per-Packet Security Inputs
Although K_enc and K_mic are stable for a given node pair, each packet still carries per-packet security inputs in SECINFO.
These inputs are:
- the 4-byte frame counter
- the optional 2-byte salt
These values are not used to derive the pairwise keys. Instead, they are used as packet-specific inputs to encryption, authentication, and replay protection.
For encrypted packets:
K_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 must not rely on the construction’s resistance to nonce misuse as a substitute for replay detection.
Multicast Packet Keys
For multicast, the configured channel key is the base secret. The encryption and authentication keys are derived once and are stable for a given channel.
ikm = channel_key
salt = "UMSH-MCAST-SALT"
info = "UMSH-MCAST-V2" || channel_id
okm = HKDF-SHA256(ikm, salt, info, 64)
K_mic = okm[0..32]
K_enc = okm[32..64]
As with the pairwise keys, okm is exactly the RFC 5297 AES-SIV key for the channel: S2V key first, CTR key second.
These keys are stable for the channel and may be cached by the implementation. They do not change from packet to packet. Per-packet variability is provided by the frame counter and optional salt in SECINFO, which serve as inputs to encryption and replay detection (see Per-Packet Security Inputs).
Encrypted Packets
When encryption is enabled, UMSH uses AES-SIV (RFC 5297) with AES-256.
The processing is:
- Compute the full 16-byte synthetic IV
V = S2V(K_mic, S1, S2)per RFC 5297 §2.4, whereS1is the canonical associated data (see Associated Data) andS2is the plaintext. - Truncate
Vto the MIC length specified by the SCF; this is the on-wire MIC. - Construct the CTR IV from the MIC (see CTR IV Construction).
- Encrypt the plaintext using AES-256-CTR with
K_encand the constructed IV. The counter is the full 16-byte IV, incremented as a 128-bit big-endian integer (RFC 5297 §2.5).
The MIC is transmitted at the end of the packet rather than prepended as in RFC 5297, allowing its length to be controlled independently via the SCF MIC size field.
With a 16-byte MIC, the result is byte-for-byte AEAD_AES_SIV_CMAC_512 (RFC 5297 §6.3): the key is K = K_mic || K_enc, the canonical AAD is the single associated-data component, and the CTR IV construction reduces to the RFC’s initial counter Q. Shorter MIC lengths use the same computation and differ only in how the CTR IV is filled.
As in RFC 5297, the synthetic IV binds the ciphertext to the key, associated data, and plaintext, so there is no caller-supplied nonce to misuse: repeating all three inputs yields an identical packet and reveals only the repetition itself. Truncated MICs weaken this bound in proportion to the truncation, since the CTR IV is reconstructed from the truncated MIC and SECINFO rather than the full V.
CTR IV Construction
The 16-byte CTR IV is constructed by appending the SECINFO field to the MIC, zero-padding or truncating the result to exactly 16 bytes, and then clearing the top bit of bytes 8 and 12 as RFC 5297 §2.6 does when forming the initial counter:
IV = truncate_or_pad_to_16( MIC || SECINFO )
IV[8] = IV[8] & 0x7F
IV[12] = IV[12] & 0x7F
The bit-clearing step is applied unconditionally, whatever bytes—MIC, SECINFO, or zero padding—occupy positions 8 and 12.
For the 16-byte MIC, SECINFO is entirely truncated away and the IV is the masked synthetic IV—exactly the initial counter Q from RFC 5297 §2.6. For shorter MICs, the IV incorporates the frame counter and optional salt from SECINFO, providing additional per-packet IV variability.
| MIC Length | SECINFO (5 B) | SECINFO (7 B) | SECINFO bytes in IV |
|---|---|---|---|
| 16 B | truncate to 16 | truncate to 16 | 0 (IV = masked 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 computed exactly as for encrypted packets—the truncated S2V(K_mic, S1 = AAD, S2 = payload)—and the encryption step is simply omitted.
Associated Data
The associated data (AAD) binds the immutable header fields to the MIC so that any modification is detected. The canonical AAD enters S2V as the single associated-data component S1; the plaintext (or, for unencrypted packets, the payload) is the final component S2.
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:
- Take the full 16-byte S2V output
V(the same value used to produce the packet MIC, before any truncation). - Encrypt
Vwith a single AES-256-ECB block encryption using the pairwiseK_enc. - Truncate the result to 4 bytes.
ack_tag = truncate_to_4( AES-256-ECB( key=K_enc, block=V ) )
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)Vis the full 16-byte S2V output 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-256-CTR with the channel’s derived encryption key K_enc_channel (see Multicast Packet Keys), using the CTR IV constructed from the packet MIC and SECINFO (see CTR IV Construction).
Let:
K_enc_channel= channel encryption key derived from the channel key via 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-256-CTR( key=K_enc_channel, iv=IV, plaintext=DST || SRC )
This allows a receiver possessing the channel key to recover both the destination (to confirm the packet is addressed to them) and the source address (to derive the pairwise keys needed to authenticate and decrypt the payload).
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 packet carries a trace-route option, each forwarding repeater prepends its own hint, on routed hops as well as flooded ones. 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.
Knowing the path forward is not the same as the destination knowing the path back. A source-routed packet arrives with its hints consumed, so nothing on it describes the return direction, and a destination with no cached route composes a reply that no repeater may carry. A sender that source-routes a packet requesting an acknowledgement should therefore include a trace-route option as well, unless it already knows the destination can reach it. The routed hops record themselves, and the acknowledgement has a path home.
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. A bridge carries a packet between the repeaters at either end of it, and those repeaters do the forwarding: each consumes source-route hints, accounts for a hop, and records itself in a trace exactly as it would for a packet it heard off the air. A trace route that crosses a bridge therefore contains both of their router hints, and source-routed packets traverse the bridge transparently.
Because a repeater at each end forwards the packet, a crossing spends two flood hops rather than one. The repeater on the inbound side transmits on the medium the packet arrived from, which is the ordinary forwarding confirmation the previous hop listens for; a bridge needs no retransmission of its own to provide it.
Flooding works across bridges, and the two hops a crossing spends are what keep individual meshes local and accountable while still enabling multi-segment routing. A bridge MAY additionally clamp the remaining flood hop count of packets that transit it, which lets an operator narrow a bridge’s reach without reconfiguring the nodes behind it.
Caution
Internet bridges have the potential to be destructive to the mesh and are generally discouraged because 1) they cannot be relied upon in an emergency, and 2) they can waste airtime with useless, non-local chatter. Moreso than other types of bridges, internet bridges MUST limit what transits them: the flood hop count is bounded by the two hops a crossing spends, and deployments SHOULD rate-limit each participant and MAY clamp the hop count further.
A client–server tunnel realization of an internet bridge is specified in Internet Bridging.
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 and RSSI, so that better-positioned repeaters transmit first and the rest can suppress their retransmission if they overhear an earlier forward.
See Channel Access.
Packet Processing
This chapter describes how a receiving node processes an incoming packet. This procedure applies to all nodes, not just repeaters. Repeater-specific forwarding logic is described separately in Repeater Operation.
A packet enters this procedure however it reached the node. Most arrive off the air, but a node whose radio is shared with another stack also receives what that stack transmits, and a node reachable over a point-to-point link—a backhauled host, for instance—receives what arrives across it. Such a packet is processed exactly as a received one, with one difference: it carries no signal measurements, so anything defined in terms of how well the packet was heard does not apply to it.
Receive Procedure
-
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 S2V output 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 Address Encryption for details.
In both cases, the channel key is the membership credential—possessing it is both necessary and sufficient to participate.
Channel Keys
A channel key is a 32-byte symmetric key. It serves as the root secret from which encryption, authentication, and identification keys are derived (see Multicast Packet Keys).
How a node obtains a channel key depends on the type of channel—see Joining a Channel below.
Channel Identifier
Each channel is identified on the wire by the first 2 bytes of the 16-byte channel identifier derived from the channel key. What travels on the wire is a compact hint that allows receivers to quickly identify candidate channels without attempting decryption with every configured key. Like destination hints, it is not cryptographically authoritative—collisions are possible and must be resolved by attempting cryptographic verification. The full identifier is where a collision cannot be tolerated: it names a channel to a management interface without disclosing the key.
Encrypted and Unencrypted Modes
Channel-addressed packets (multicast and blind unicast) may be sent with or without encryption, controlled by the E flag in the Security Control Field.
-
Encrypted (E=1): The source address is encrypted together with the payload, concealing the sender’s identity from observers who do not possess the channel key. Only channel members can recover the source address and payload. See Encrypted Multicast for the packet layout.
-
Unencrypted (E=0): The source address and payload appear in cleartext, but the packet is still authenticated with a MIC derived from the channel key. This mode is useful for amateur radio operation or other contexts where encryption is not permitted. See Unencrypted Multicast for the packet layout.
Multi-Hop Delivery
Channel-addressed packets are delivered via flood forwarding, bounded by the optional flood hop count field. Repeaters forward these packets according to the standard forwarding procedure, including duplicate suppression, signal-quality filtering, and region-scoped flooding.
Sender Authentication
Multicast authentication is based on the shared channel key, not on individual sender identity. The MIC proves that the sender possesses the channel key, but any channel member can construct a valid packet with any claimed source address. This is a fundamental property of symmetric-key multicast—see Multicast Sender Authentication for further discussion.
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 [T_frame/40, T_frame/4].
- Perform CAD again.
- Repeat up to 15 more times (16 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 proportional to the power of the received signal yet also inversely proportional to the quality of the received signal. Nodes that heard the packet cleanly but faintly transmit first; nodes that barely met the signal threshold, or heard it very strongly, 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:
SNR_low = -9 dB
SNR_high = 3 dB
RSSI_low = -100 dBm
RSSI_high = -70 dBm
W_min = 0
W_max = T_frame/2
W_jitter = T_frame/10
quality = clamp((received_SNR − SNR_low) / (SNR_high − SNR_low), 0, 1)
signal = clamp((received_RSSI − RSSI_low) / (RSSI_high − RSSI_low), 0, 1)
W = W_min + (W_max − W_min) × max((1 − quality), signal)
delay = D_ack + W + uniform_random(0, W_jitter)
Where:
- When flood-forwarding, the effective minimum SNR threshold is the higher of the Minimum SNR packet option (if present) and any locally configured minimum SNR. A repeater MUST NOT flood-forward if the received SNR is below that effective threshold. (Signal-quality thresholds do not apply to source-routed hops.)
SNR_low/SNR_highandRSSI_low/RSSI_highdefine the clamp ranges that normalize the two measurements for the contention heuristic.W_minis the minimum contention window for strong receptions.W_maxis the maximum intentional forwarding-delay window.W_jitterbounds the random tie-breaking delay added after the deterministic window, so that nodes whose measurements agree do not transmit in the same instant.received_SNRandreceived_RSSIare the SNR and RSSI 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.
After computing the delay, the repeater waits. Other packets SHOULD continue to be forwarded while waiting, assuming the channel is clear.
If the repeater overhears the same packet forwarded by another node (identified by MIC in the duplicate cache) before the delay expires, it SHOULD defer rather than transmit. A safe default is to recalculate a new delay using the same W_min/W_max limits—including D_ack when it applies, since the overheard copy may itself elicit an immediate ACK from the destination—and restart the waiting period. 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_max + W_jitterof 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 contention delay W + uniform_random(0, W_jitter) may be arbitrarily small, CAD detects preamble energy and may miss an ACK already past its preamble, and a forwarder may not be able to hear the destination at all.
D_ack therefore provides deterministic separation. When the packet being flood-forwarded requests an ACK (UNAR or BUAR) and was received with no remaining source-route hops—the conditions under which its destination transmits an immediate ACK—the forwarder MUST delay by at least D_ack before transmitting, in addition to the computed contention delay. D_ack SHOULD cover the destination’s receive-to-transmit turnaround plus the on-air duration of a MAC Ack packet at the configured channel settings. The suggested default is 0.25 × T_frame; implementations that compute the actual MAC Ack airtime MAY use a tighter bound.
A packet received with source-route hops still pending does not elicit an immediate ACK from its destination, so D_ack does not apply when forwarding it.
Immediate ACK Transmission
When a node is the final destination of an ack-requested packet (UNAR or BUAR) and the packet has no remaining source route hops, the node SHOULD transmit the ACK immediately—without performing CAD—provided the radio is available for transmission. This is warranted because the channel is known to have been clear at the moment the received packet ended, and flood forwarders hold their transmissions back by the ACK protection interval so the ACK gets first use of the channel.
If the radio is not immediately available for transmission, the node SHOULD perform normal CAD and backoff before transmitting the ACK.
Repeater Operation
Forwarding logic is intentionally conservative. A repeater should evaluate packets in the following order.
Routing Invariants
The routing model is governed by a few simple rules:
- Every currently defined on-mesh packet type is routable.
- In the current protocol this includes broadcast, MAC ack, unicast, multicast, and blind unicast packets.
- Reserved or opaque packet types are not routable until the protocol defines their forwarding semantics (there is only one at the moment, type 5).
- A repeater forwards other nodes’ traffic, never its own and never traffic that has arrived.
- A repeater MUST NOT forward a packet whose source address identifies one of its own identities, by hint or in full-key form. Re-flooding its own transmission wastes airtime, and prepending its 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.
MAC-ack entries are the exception: their lifetime SHOULD be on the order of tens of seconds—the scale of a sender’s retry ladder—not an hour. An identical re-acknowledgement is the one duplicate a correct node emits deliberately (see the duplicate acknowledgement window), and it recovers a lost ack only if repeaters carry it; under a lifetime longer than the sender’s recovery horizon, the first forward of an ack absorbs every later one and the recovery path dies at the first hop. A short lifetime still collapses the copies of any single exchange, which play out within a few confirmation windows.
A repeat of a key already held MUST NOT extend that entry’s lifetime. Refreshing the timestamp on each sighting would let a node repeating itself inside the window hold its own suppression open indefinitely, which is the behavior expiry exists to prevent.
Cache Sizing
Each cache entry is small (equal to the cache key size—typically 4 to 16 bytes), so generous sizing is inexpensive. The recommended minimum is 32 entries; the suggested default is 64 entries. High-traffic deployments or networks with large diameters may benefit from 128 or more entries.
Forwarding Procedure
-
Duplicate suppression
- If this packet was forwarded recently, do not forward.
-
Local origin and local destination
- If this repeater’s own radio already transmitted this packet, do not forward. A repeater that shares its radio with another stack—an attached host, most commonly—receives a copy of what that stack transmits. Forwarding it would put a packet back on the air that this antenna already sent, and the source address belongs to the other stack, so the check below cannot catch it. The repeater MUST insert the cache key as if it had forwarded the packet: a neighbor’s repeat arrives shortly after, and it is the same packet, already carried.
- If the packet’s source address identifies one of this repeater’s own identities, do not forward.
- If the packet’s destination address identifies one of this repeater’s own identities, do not forward.
-
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.
- These thresholds ask how well the packet was heard. A packet handed to the repeater over a point-to-point link rather than a radio was not heard at all, and carries no measurement to compare; the thresholds do not apply to it.
-
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
- The contention window staggers the repeaters that all heard one transmission. A packet that arrived over a point-to-point link was heard by this repeater alone, so there is no contention to resolve and no window to wait out.
- Forward the modified packet according to normal channel access rules.
A packet that arrives carrying an empty source-route option matched no hint at this repeater, so it takes the flood path: steps 7 through 9 apply in full. This is how a hybrid route transitions to flooding—the transition is observed by the repeater after the one that emptied the route, not performed by it.
A bridge rewrites nothing of its own: a repeater at each end of it forwards the packet by this procedure, so a crossing applies these rules twice.
Forwarding Confirmation
Repeaters do not generate MAC acks—acks are generated only by the final destination. Instead, a node can passively confirm that a transmitted or forwarded packet was received by listening for a subsequent retransmission of the same packet (or it’s ack).
This applies to:
- Source-routed packets: Each forwarding hop listens for the next hop—the node matching the next source-route hint—to retransmit.
- Flood originators: The originating node listens for any node to retransmit.
- Flood repeaters: Intermediate flood-forwarding nodes MUST NOT retry. Multiple nodes may forward the same flood packet, and a repeater has no designated next hop to listen for; retrying would increase congestion without improving reliability.
- Routed MAC acks: An ack that carries a source route or flood budget is a routed send like any other; the destination that produced it listens for the first hop to carry it onward and retries on silence. Retrying the ack first is what spares the sender a full data retransmission when only the ack’s first hop failed.
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 + W_jitter + D_ack
where W_max and W_jitter bound the intentional forwarding delay permitted for the path and D_ack is the ACK protection interval when it applies. With the suggested defaults W_max = T_frame/2, W_jitter = T_frame/10, and D_ack = 0.25 × T_frame, this yields confirm_timeout = 2.85 × 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:
- recalculate 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: The trace Node A sent taught Node B a path back, and nothing else. A node responding to a packet that carried a trace-route option SHOULD carry one on its response, whatever form that response takes—MAC ack, beacon, or application payload. Where the response is a MAC ack, that ack is the whole of what Node A receives, so an ack without a trace leaves Node A holding no route to Node B at all.
A sender decides whether to originate the option from what it already knows about the destination. One that holds no path—no source route, and no evidence the destination is a direct neighbor—SHOULD include a trace route: the packet is going to flood regardless, and the trace is what turns that flood into a path. A sender following a source route SHOULD NOT, since that path is already known and re-recording it on every packet is the proactive refresh this specification does not define. That applies to a path the sender holds, which is what makes the re-recording redundant. A response steered down the trace its own request accumulated—the Identity Request answered from a source route built out of the trace, for one—is following the requester’s path rather than one either side had, and the response rule above governs: the requester holds nothing until the response records something.
A packet carrying neither flood hops nor a source route SHOULD NOT carry a trace route at all, whatever the sender knows. No repeater may forward such a packet, so the option can only arrive as empty as it left, and its arrival already proves what an empty trace would have said.
Because router hints are only two bytes, different repeaters may share the same hint, which may result in redundant (but harmless) forwarding along a source route.
Route Learning
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. -
Neither: a packet that arrives carrying no flood hop count and no source route was one that no repeater had permission to forward, so it reached the receiver off the sender’s own transmitter. The node SHOULD cache the sender as a direct neighbor. This is the same conclusion an empty trace route supports, drawn from the packet’s structure rather than from an option, which is what lets an unforwardable packet leave the trace route off.
A MAC ack is such a packet. It names no source, but its ack trailer correlates it to an outstanding request and so to the peer that sent it, and whatever routing evidence it carries updates that peer’s routing state like any other packet’s would.
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 one flood hop more than the source route has hints
- 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 Request |
| 9 | Node Management Response |
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 Req | Yes | No | No |
| Node Management Resp | 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.
Response Carriage
A response to a unicast request is carried the same way the request was: a request that arrived as blind unicast on a channel is answered as blind unicast on that same channel, and a request that arrived as plain unicast is answered as plain unicast. This holds for every request and response the protocol defines—MAC commands, node identity, node management—and applies whether the response comes from the MAC layer or from an application above it.
Blind unicast conceals both endpoints from observers who lack the channel key. A response sent off the channel names the parties the request took care to hide, so it would undo that concealment for the exchange as a whole. A MAC acknowledgement is not a response and remains channel-less; it carries no destination hint and so names neither party.
A node answering a multicast or broadcast request is not bound by this rule, since such a request has no pairwise carriage to mirror. What a node may answer with is left to each command’s own definition.
In-Band Node Management
Nodes may optionally support remote management via Node Management Request and Node Management Response payloads, which carry ULCP exchanges over the mesh itself. The payload format, authorization model, and reachable state are specified in Node Management; support is advertised through the CAP_ADMIN ULCP capability.
Node Identity
The node identity payload is an application-layer structure carried inside the UMSH payload. Its contents—including the timestamp option below—are not interpreted or required by the MAC layer. The MAC layer itself is timestamp-free (see Frame Counter).
Structure
+------+------+---------+------+-----------+
| ROLE | CAPS | OPTIONS | 0xFF | SIGNATURE |
+------+------+---------+------+-----------+
1 B 1 B variable 1 B 64 B
Fields:
ROLE(1 byte)—the node’s primary role.CAPS(1 byte)—the node’s capability bitmap.OPTIONS(variable, optional)—a CoAP-style option list of node identity options, using the same delta-length encoding as packet options.0xFF(1 byte, optional)—options-terminator marker. Present only when a signature follows.SIGNATURE(64 bytes, optional)—EdDSA signature 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 option per region, its UTF-8 string form, max 24 bytes each |
| 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 regions the node will flood-forward for, one option per region. Each entry carries the region’s string form—the short code for a region that has one, the region name otherwise—rather than its two-byte region code: the code is always derivable from the string, while a hash-derived code cannot be turned back into a name. A sender lists at most 10 regions, and the identity payload must still fit its enclosing packet, so a node whose full list does not fit omits regions; the option names regions the node forwards for without promising to name every one. A node that omits the option entirely makes no claim about its regional forwarding policy. Entries carry the capitalization the region was written with, and a receiver comparing them derives their codes, which ignores ASCII case. Max length: 24 bytes per entry, the bound on region names.
Nonce (option 5)
Copied verbatim from the Identity Request that solicited this identity payload, letting the requester correlate the response to its request. Present only in responses whose request carried a NONCE option. Length: 4 bytes.
Variable-Precision Location Format
The node location is encoded as a string of one or more bytes, where each byte narrows the position to a 16×16 sub-grid of the preceding byte’s cell. Additional bytes increase precision; trailing bytes may be omitted to give a coarser—and therefore more privacy-preserving—location.
Grid Subdivision
Each byte splits its parent cell into a 16×16 grid of children. Within each byte, the high nibble indexes along latitude and the low nibble indexes along longitude:
7 6 5 4 3 2 1 0
+---------------+---------------+
| LAT NIBBLE | LON NIBBLE |
+---------------+---------------+
4 bits 4 bits
The first byte subdivides the entire globe (latitude in 16 slices of 11.25°, longitude in 16 slices of 22.5°). Each subsequent byte subdivides the cell selected by the byte before it, using the same high-nibble-latitude, low-nibble-longitude convention.
Latitude leads here for the same reason it leads everywhere else in UMSH: a coordinate pair is written, spoken, and passed as (latitude, longitude), and an encoding that reversed the pair would be the one place the convention did not hold.
Encoding a Location
Given latitude LAT in degrees (-90..+90) and longitude LON in degrees (-180..+180), an N-byte code can be derived either in a single step or byte by byte. The direct form is normative; where floating-point rounding makes the iterative form disagree at the final nibble, the direct form’s result is the correct code.
Direct form
Compute two 4N-bit indices over the full desired precision:
lat_index = floor((LAT + 90) × 16^N / 180)lon_index = floor((LON + 180) × 16^N / 360)
Then read nibbles from most significant to least significant:
- byte k high nibble =
(lat_index >> (4 × (N − 1 − k))) & 0xF - byte k low nibble =
(lon_index >> (4 × (N − 1 − k))) & 0xF
This form makes the hierarchy explicit: truncating an N-byte code to k bytes yields exactly the k-byte code for the same position.
Edge cases: LON = +180° is equivalent to LON = −180° and wraps lon_index to 0. LAT = +90° is a single degenerate point; clamp lat_index to 16^N − 1.
Iterative form
Emitting one byte at a time, with lat_step = 11.25° / 16^k and lon_step = 22.5° / 16^k:
- byte k high nibble =
floor(((LAT + 90) mod (16 × lat_step)) / lat_step) - byte k low nibble =
floor(((LON + 180) mod (16 × lon_step)) / lon_step)
For byte 0 (k = 0), lat_step = 11.25° and lon_step = 22.5°, so the modulus is a no-op for valid inputs and the formulas reduce to:
high_nibble = floor((LAT + 90) / 11.25)low_nibble = floor((LON + 180) / 22.5)
The same edge cases apply as in the direct form: LON = +180° wraps to nibble 0 naturally via the modulus, but LAT = +90° must be clamped to nibble 0xF at every position—the modulus would otherwise wrap it to nibble 0.
Worked Example
Encode (LAT, LON) = (37.331°, −121.883°) (San Jose, CA) at 3-byte precision.
Direct form:
lat_index = floor(( 37.331 + 90) × 4096 / 180) = floor(2897.49) = 2897 = 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.
MAC commands are addressed to a single node. Unless a command’s definition provides rules for multicast or broadcast use, as Identity Request does, a node ignores a command that arrives by multicast or broadcast.
A node answering a command carries its response the way the request was carried, as Response Carriage defines.
Command Registry
| 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 |
| 10 | Peer Repeaters Request | Request |
| 11 | Peer Repeaters Response | Response |
Identity Request (1)
Requests that the destination respond with its node identity.
A common use is resolving a node hint to a full address. A node that knows only a peer’s hint sends this command as a broadcast or multicast carrying a FILTER_NODE_HINT filter; a matching node replies with an encrypted unicast (or blind unicast) response carrying its full node identity. When the requester believes the peer may not yet know the requester’s own address, it sends its full source address in the request so the peer can reply directly.
Because a broadcast Identity Request can solicit many replies:
- A node MAY decline to respond to a request from an unknown source.
- A repeater MAY decline to forward a broadcast Identity Request, particularly when its filters are broad enough to solicit a large number of replies.
In order to manage the potential flood of responses, the following rules MUST be applied for broadcast (always) and (by default) multicast:
- The Route option must either be absent or empty, otherwise the request should be dropped.
- Responses must be delayed by a random amount of time drawn from a window of 0.5 to 30 seconds, so that the selected nodes do not all answer at once. A delayed response that then fails channel-activity assessment follows the responder’s normal bounded CCA backoff-and-retry before being dropped. A request every FILTER_NODE_HINT of which is 2 or 3 bytes is exempt and MUST be answered without the delay: such a request names a node rather than a share of the mesh, so there is no crowd of replies to spread and the hold would only make the answer late. One byte names a 256th of what the request reaches, so it is not exempt; filters of the same type combine as OR, and the shortest one decides.
- One solicitation is answered at most once. A request reaches a node once per path it travels, and a plain broadcast carries no frame counter for a lower layer to recognize the repeat by; the NONCE is what names the solicitation. A responder suppresses a request matching one it has already answered—same sender, same NONCE—for at least as long as it may hold the reply. A request carrying no NONCE cannot be distinguished from a repeat of itself; a requester that wants a further answer inside that window asks with a fresh NONCE.
A FILTER_NODE_HINT filter names a single node, so a request carrying one solicits a single reply however far it travels. A partial hint names a small set rather than one node, and the shorter it is the larger that set: a requester sends the longest hint it holds. A request with no hint filter at all selects by role or capability, and every node it reaches may answer; such a request is therefore confined to the requester’s own neighborhood:
- The FHOPS byte must either be absent or set to 0x00. A request that is flood routed—FHOPS present with either nibble nonzero—MUST NOT be answered.
- The response MUST NOT carry a FHOPS field (the FCF flood hop count flag is clear). The invariant is that the reply travels exactly as far as the question did and no further: a request that crossed one hop is answered across that one hop, and a request steered along a Source Route option is answered back along the path it was steered down.
A request steered by a Route option reaches its destination neighborhood with that option emptied, since each repeater consumes its own hint. The nodes there are strangers to the requester: they hold no route to it, and their replies carry no flood budget for a repeater to spend. Such a request therefore carries a Trace Route option, and a node answering it sends the response as a source route built from the trace the request arrived with. Repeaters prepend when forwarding, so the accumulated trace already reads as the path back and is used as-is, without reversal.
The response carries a Trace Route option of its own, and a Trace
Signal where the request carried one.
The trace on the request is what taught the responder a path home, and it taught
the requester nothing: a broadcast solicitation leaves no route behind at the
requester, and the response reaches it with its own Route option consumed and its
FHOPS_ACC counting only the tail of the path. Without a trace on the response
the requester ends the exchange holding no route to the node it just identified.
This is the general rule for a response to a traced packet (Path
Discovery) rather than an exception to the advice
against tracing a source-routed packet: the path a response is steered down is
the requester’s own trace, not a route either side already held.
A repeater consumes its own hint while forwarding, not while receiving, so a request whose Route option still names a repeater is one that repeater drops. Asking a named router to identify itself therefore means steering the request to the hop before it and narrowing it with that router’s two-byte hint: the request arrives with an empty Route option, and the router answers it as any other node in that neighborhood would.
These requirements MAY be relaxed for specific private channels, but MUST remain in place for all public channels. Note that these requirements are specifically designed to allow discovering identities from a specific repeater location on the mesh network.
An Identity Request is answered with a targeted unicast identity response, never by flooding an advertisement to the whole network.
Identity Request Options
A unicast Identity Request requires no payload. A multicast or broadcast request MUST carry at least one filter option, so that only the intended nodes respond.
Options use the CoAP-style delta-length encoding defined in Packet Options. As in CoAP, an option’s key encodes its criticality: odd-numbered keys (least-significant bit set) are critical, even-numbered keys are elective. A node that encounters a critical option it does not understand MUST treat itself as excluded and MUST NOT respond.
Filter options select which nodes respond. They combine as a logical AND across different filter types and a logical OR among repeated filters of the same type: a node responds only if it satisfies every filter type present, and it satisfies a given filter type if it matches any one of that type’s values. Non-filter options (such as NONCE) do not participate in this matching.
| 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 | 1–3 bytes | Respond only if this is a leading part of the responder’s own node hint. A 3-byte value is the whole hint; a 2-byte value is the router hint, which is all a Source Route or Trace Route reveals about the hops it names. A zero-length value matches nothing. |
| 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 |
The response SHOULD carry the same trace route and trace signal options the request carried. An echo measures a path, and a response traced differently from the request measures a different one; pairing the two options is what makes the measurement per-hop rather than end to end.
PFS Session Request (6)
Initiates a PFS session. The sender generates a fresh ephemeral node address and transmits it along with a requested session duration. See Perfect Forward Secrecy Sessions for the session establishment mechanism, key derivation, and wire-level privacy properties.
| 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.
Peer Repeaters Request (10)
Asks the destination—typically a repeater—for its list of known peer repeaters.
The list is the responder’s repeater neighborhood: the repeaters it has heard from their own transmitters. A repeater enters it in one of two ways. Forwarding a frame the responder receives is one—a repeater prepends its router hint to the frame’s Trace Route, so the first hint names the repeater whose transmission was just heard. Advertising is the other—a node identity claiming the repeater role or capability, heard directly rather than forwarded. Frames heard from nodes that are not repeaters, and identities that arrived through a repeater, put nothing in the list. A neighbor need not be on the air: a repeater reached over a point-to-point link, such as an attached host, is listed the same way and simply carries no signal measurements.
The command-specific payload is a CoAP-style option list, using the delta-length encoding defined in Packet Options:
| Number | Name | Value | Description |
|---|---|---|---|
| 0 | Nonce | 2 bytes | Correlation identifier the responder MUST echo in its response’s Nonce option. |
| 1 | Cursor | variable | Resume token, copied verbatim from the Cursor option of a previous response. Absent on the first request of an enumeration. |
No payload follows the options, so the sender omits the 0xFF end-of-options marker.
A list too large for one response is retrieved page by page: the first request carries no Cursor, and each response that carries one names the place a follow-up request should resume from. The cursor is opaque to the requester; only the responder gives it meaning.
Peer Repeaters Response (11)
Returns one page of the responder’s peer-repeater list in response to a Peer Repeaters Request:
+---------+------+---------+------+---------+------+ +---------+--------+
| OPTIONS | 0xFF | ENTRY 0 | 0xFF | ENTRY 1 | 0xFF | ... | ENTRY N | (0xFF) |
+---------+------+---------+------+---------+------+ +---------+--------+
The response options:
| Number | Name | Value | Description |
|---|---|---|---|
| 0 | Nonce | 2 bytes | Copied verbatim from the request. Present only when the request carried a Nonce. |
| 1 | Cursor | variable | Opaque resume token for the next page. Present when further entries remain; the final page carries no Cursor. |
| 2 | Total | 1 byte | Unsigned count of entries in the full list, not the page. Required in the response to a cursorless request; optional on later pages. |
The 0xFF after the response options is the ordinary end-of-options marker; the entry list that follows is the command’s payload. Each entry is itself a CoAP-style option list terminated by its own 0xFF byte. The final entry MAY omit its terminator, consistent with omitting the marker when nothing follows.
The options per entry:
| Number | Name | Value | Description |
|---|---|---|---|
| 0 | Node Hint | 2 or 3 bytes | The peer’s node hint—3 bytes when the responder holds it in full, or the 2-byte router hint when that is all it has observed. The only required option. |
| 1 | Node Name | UTF-8, max 24 bytes | The peer’s display name, as learned from its identity. |
| 2 | RSSI/SNR | 2 bytes | Signal measurements from the most recent reception: RSSI as an unsigned value representing negative dBm (e.g. 130 = −130 dBm), then SNR as a signed value in quarter-dB steps. |
| 3 | Last Heard | 1–2 bytes | Minutes since the responder last heard the peer, as a minimal big-endian unsigned integer. Saturates at 65535 (about 45 days). |
| 4 | Location | 1–7 bytes | The peer’s position, in the variable-precision location format. |
| 5 | Supported Flood Regions | n × 2 bytes | Concatenated 2-byte region codes the peer flood-forwards for. |
An option whose value the responder does not know is omitted. Entries carry region codes rather than the strings the identity option carries—the entry format is tighter on space, and the string form of a code, when one is wanted, is available from the peer’s own identity.
Node Management
![NOTE] This section is an early work in progress and this protocol may change significantly.
A node that supports node management can be configured and observed over the
mesh itself, using the same command grammar, property model, and numeric
registries that ULCP defines for the local link. Node
Management Request (payload type 8) and Node Management Response
(payload type 9) payloads carry ordinary ULCP frames between an
administrator—a node listed in the device’s
administrator list—and the device, in unicast
packets exchanged with the device identity.
Support is optional and advertised through CAP_ADMIN.
Administering a device over the mesh reaches the device domain and nothing else. An administrator is not a tethered host: a remote exchange is not an attach, touches no session state, and neither sees nor disturbs the host domain or the assistance the device owes to whatever host it serves. Two Kinds of Attach draws the same boundary on the local link; over the mesh, only the administrative kind exists.
Transport Properties
The binding relies on exactly what the MAC layer guarantees for secure unicast: the source of every accepted packet is authenticated, payloads are confidential, and replay protection accepts a given frame at most once. It assumes nothing more—not delivery, and not ordering. The payload format adds what the ULCP grammar needs on such a transport:
- a token correlates responses with requests across long and variable round trips, in place of the TID of the local bindings;
- retained responses make retransmission safe: a repeated request is answered again, not executed again;
- cursors carry responses larger than one frame across as many exchanges as needed, without per-read state on the device.
Several operations per exchange need nothing from the envelope: the multi-property commands already carry several reads, or several ordered writes, in a single frame.
Because wire-level duplicates are impossible, the binding has no deduplication window of its own; the only duplicates that can exist are an administrator’s own retransmissions, which the token identifies.
Payload Format
Request and Response payloads share one format, consisting of, following the payload type byte:
+-------+---------+------+----------+
| TOKEN | OPTIONS | 0xFF | FRAME |
+-------+---------+------+----------+
2 B variable 1 B variable
Direction lives entirely in the payload type. A device drops a Response payload—it never solicits anything—and an administrator that receives a Response matching no outstanding exchange of its own discards it, both with accounting.
Node Management payloads travel only in unicast and blind unicast packets: Requests are addressed to the device identity, and Responses return to the requesting node on the carriage the Request arrived on (see Response Carriage), using whatever routing state the exchange has supplied (see Route Learning). A device drops a Node Management payload arriving by multicast or broadcast, with accounting.
Token
Two opaque octets chosen by the administrator and echoed verbatim in the response. The token correlates a response with its request and identifies retransmissions (see Retries and At-Most-Once Processing). An administrator MUST choose a token different from its previous exchange’s when beginning a new exchange, and MUST reuse the token when retransmitting a request unchanged.
Options
Options use the CoAP-style delta-length encoding defined in
Packet Options. As in
MAC command options,
odd-numbered options are critical and even-numbered options are
elective. A device that receives a request carrying an unrecognized
critical option answers with PROP_LAST_STATUS of STATUS_UNIMPLEMENTED
and does not process the request; unrecognized elective options are
ignored. An administrator that receives a response carrying an
unrecognized critical option treats the exchange as failed.
| 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
follows.
Frame
Exactly one ULCP frame, extending to the end of the payload—the payload
bounds it, so it carries no length prefix. The embedded frame uses the
exact frame format of the local bindings, so
a device dispatches it through the same machinery that serves its local
link. Senders MUST set its TID bits to zero, and receivers ignore them:
correlation is by token. A request whose frame is absent or cannot be
parsed is answered with a CMD_PROP_IS of PROP_LAST_STATUS reporting
STATUS_PARSE_ERROR.
The payload, envelope included, must fit a single UMSH frame; there is no fragmentation. The unassigned option numbers are this format’s growth space: a future need—carrying a request larger than one frame, say—is met by assigning a critical option, and existing devices already reject what they do not recognize.
Exchanges
Every interaction is an exchange: one request payload from an
administrator, one response payload from the device. The device sends
nothing over this binding except in response to a request—CMD_PROP_IS
in its unsolicited role, and CMD_PROP_INSERTED and CMD_PROP_REMOVED as
spontaneous notifications, do not occur here. State an administrator cares
about is read, not pushed.
The Response echoes the token and carries one frame: exactly
the frame the device would emit in reply on a local binding—a
CMD_PROP_IS, CMD_PROP_INSERTED, CMD_PROP_REMOVED, or CMD_PROP_ARE
on success, or a CMD_PROP_IS of PROP_LAST_STATUS reporting the error.
A request frame carrying a Device→Host command is answered
STATUS_INVALID_COMMAND. Long-running operations report
STATUS_IN_PROGRESS as on any binding; the administrator observes
completion by reading state in a later exchange.
Multi-Property Requests
Several operations travel in one exchange through the multi-property
commands, whose semantics this binding leaves untouched:
CMD_PROP_MULTI_GET reads several
properties, continuing past per-property failures, and
CMD_PROP_MULTI_SET applies writes
strictly in order, stopping at the first failure—which is how an
administrator expresses writes whose effects depend on sequence.
CAP_ADMIN requires CAP_CMD_MULTI, so an administrator may rely on
both.
One rule is the binding’s own: the device does not execute a
CMD_PROP_MULTI_SET entry whose reply entry would not fit the remaining
space in the response payload, and stops there exactly as an error would
stop it. An administrator that receives fewer reply entries than it sent
examines the last entry it did receive: an error means the sequence
stopped on that failure; a success means it stopped for space, and the
administrator reissues the remainder as a new exchange.
Resets
Commands that initiate a reset—CMD_RST, CMD_RESTORE in its reset
form, CMD_REBOOT, and CMD_FACTORY_RESET—
are answered by no response payload. Delivery of such a command is
confirmed by requesting a MAC acknowledgment, and its completion by a
later exchange reading PROP_LAST_STATUS for the reset code.
A device that restarts is unreachable for as long as it takes to come
back, and a repeater that restarts takes its stretch of the network with
it. CMD_REBOOT is nonetheless within an administrator’s reach, because
the node it is most worth sending to is the one nobody can walk to.
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 read whose response does not fit one payload is completed across
several exchanges. This applies to both read requests: a CMD_PROP_GET
whose value does not fit, and a CMD_PROP_MULTI_GET whose entry list
does not fit. The response frame is well-formed but its trailing content
—the value of the CMD_PROP_IS, or the entry list of the
CMD_PROP_ARE—is a leading fragment, accompanied by a CURSOR
option: an opaque continuation handle, one to eight octets, chosen
entirely by the device. The administrator continues with a new exchange—
fresh token—whose request carries the returned cursor verbatim
alongside a repeat of the request being continued. Each response carries
the cursor to present in the next request; a response without one ends
the read, its fragment being the last. Fragment sizes are the device’s
choice, made to fill each frame; there is no fixed block size and no
position numbering.
A request carrying a CURSOR option MUST be the read being continued—the
same CMD_PROP_GET or CMD_PROP_MULTI_GET that began it. A cursor on
any other request is answered STATUS_INVALID_ARGUMENT.
The contract:
- A cursor is meaningful only to the device that issued it, and only for the request it was issued for. The administrator returns it byte-for-byte and MUST NOT construct or modify one.
- Fragment boundaries are the device’s choice and carry no meaning. The
administrator reassembles the read by concatenating the fragments in
order and parses the whole: a property value under the property’s own
rules, an entry list under
CMD_PROP_ARE’s. - Presenting the same cursor again 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, it was
issued for a different request, 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 octets 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 read everything: CMD_PROP_GET is never refused for lack
of standing, and a property the device does not serve fails exactly as it
would on the local link. PROP_CAPS in particular is readable, so
capability discovery works exactly as on the local link.
Writes are narrower. A device answers STATUS_NOT_PERMITTED—the
operation exists, and the binding is what refused it—to:
CMD_PROP_SET,CMD_PROP_INSERT, andCMD_PROP_REMOVEnaming session state or the host domain (see State Classes): that state belongs to the tethered host, and an administrator is not one. The exception isPROP_MAC_BACKHAUL: which side of the radio multiplexer the tethered host sits on is worth flipping from across the mesh, so an administrator may write it, though the host’s next attach still resets it;CMD_STR_SENDandCMD_QUEUE_DRAIN: the raw PHY stream and the receive queue are likewise the tethered host’s;- writes to
PROP_DEV_PRIVATE_KEY: a device identity cannot be installed over the mesh.
For everything else this binding meets the transport requirement of Provisioning Security: every executed request already arrives authenticated and encrypted from a listed administrator, so device-domain key material—channel keys, peer entries —may be provisioned remotely. The read-back rules are unchanged: key-bearing properties report their digest forms, never secrets.
PROP 4865: PROP_DEV_ADMINS
- Type: Multiple-Value
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_ADMIN - Item Type: 32 octets
- Post-Reset Value: Empty
Each item is the Ed25519 public key of a node authorized as an administrator of this device. Items are public keys: they carry no secret material and are reported verbatim.
An empty list disables node management entirely and is the post-reset default. The property is device-domain state: it participates in the saved snapshot like any other device-domain property, which is how a commissioned repeater stays manageable across a power cycle. It is writable over the local bindings and over this one—a listed administrator may add or remove administrators, itself included.
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 43 | CAP_ADMIN | CAP_DEV_IDENTITY, CAP_CMD_MULTI | Node management: processing of Node Management Request payloads addressed to the device identity, and PROP_DEV_ADMINS |
A device that does not advertise CAP_ADMIN drops Node Management
payloads, with accounting.
Text Messages
![NOTE] This section is an early work in progress and this protocol may change significantly.
The text message protocol carries human-readable text between nodes (unicast) or from a node to a channel (multicast).
The payload consists of a CoAP-style option list terminated by a 0xFF byte, followed by the message body. All options are non-critical—unrecognized options are ignored and the remainder of the message is displayed normally.
Message Options
Every recognized option is a singleton unless a specification explicitly declares otherwise. How a receiver treats a duplicated recognized option depends on the option’s role:
- An option that carries identity, sequencing, or reference semantics—Message Type, Message Sequence, Regarding, Editing, and extension options in the same role such as Sender Sequence—makes the message invalid when repeated, even if the repeated values are identical. The message MUST be dropped.
- A presentation option—Sender Handle, Background Color, Text Color—keeps the first occurrence when repeated; later occurrences are ignored and MAY be reported diagnostically.
- Duplicates of a zero-length flag option—Sequence Reset, Channel Group Resend—are idempotent.
Repeated unrecognized options remain ignorable.
| 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 |
| 10 | Timestamp Sent | 4-byte unix timestamp |
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.
There are five canonical reactions:
| Name | Emoji | Acceptable ASCII Representations |
|---|---|---|
| Approve | 👍 | +1 |
| Disapprove | 👎 | -1 |
| Love | ❤️ | <3 |
| Haha | 🤣 | ha, lol, haha |
| Surprise | ‼️ | !!, ! |
| Question | ❓ | ? |
A sender has at most one reaction in force per message. Sending another supersedes the previous one, and a reaction with a zero-length body withdraws it; a receiver displays the most recently received reaction from each sender (unless it was withdrawn).
Reactions reference the original message’s ID even when the message has since been edited, as Editing requires of every reference. A receiver MAY additionally tolerate a reaction that names an edit’s own ID by resolving it to the message that edit replaced.
A message resend request is itself not a message but a request to re-send a message that was inferred to exist but was not received. Such a request SHALL contain exactly one Message Type option with value 2 and exactly one Message Sequence option identifying the missing frame. A request with duplicate Message Type or Message Sequence options is invalid and MUST be ignored. It MAY also contain the Channel Group Resend flag described below and SHOULD NOT contain any other options. After validating the required options and the Channel Group Resend flag, receivers MUST ignore every other option and the entire body. Senders SHOULD use an empty body. The request MUST be received via unicast (for one-on-one messages) or blind-unicast (for channel messages), and SHALL be dropped if received via broadcast or multicast.
A resend request identifies exactly one missing frame, and a successful response retransmits exactly one frame. The 1-byte Message Sequence form requests a message by ID without specifying a fragment; the sender responds with the unfragmented message or, if it was fragmented, with fragment zero. The fragment count carried in fragment zero lets the requester ask for any remaining fragments individually. The 3-byte form requests one specific fragment.
Responses to a message resend request are optional and MAY be ignored.
Not all messages are available to be resent; for example, a requested message may have been deleted or evicted. In that case, the original sender SHOULD respond with Message Unavailable. A sender producing Message Unavailable SHOULD include only a Message Type option with value 3 and one Message Sequence option exactly matching the requested message or fragment. Its body SHOULD be empty.
Message Unavailable follows the delivery path appropriate to the requested conversation, just like a successful resend of the original message: direct-conversation responses use unicast, blind-unicast one-to-one channel responses use blind-unicast, and channel-group responses use multicast to the channel.
Once a receiver has unambiguously recognized Message Unavailable and its single Message Sequence option, it MUST ignore all other options and the entire body. The indicated sequence position is treated as accounted for, so it no longer remains a gap in that sender’s sequence history. The receiver is not required to render a whole-message unavailable response in the UI.
When the three-byte Message Sequence form identifies a fragment, only that requested fragment is reported unavailable, even if the sender no longer has any part of the message. Other fragments remain independently repairable. The receiver SHOULD render the unavailable portion of the partial message as unavailable rather than continuing to show it as pending.
Message Unavailable is also the standard response to a request for an unknown Message Sequence ID, including an ID that has not been sent.
Channel Group Resend
A zero-length flag used only on a Message Resend Request received by blind-unicast. Both a request for a message missed from a multicast channel conversation and a request for a message missed from a blind-unicast one-to-one conversation arrive by blind-unicast. The presence of this flag selects the sender’s archive for the multicast channel conversation. Its absence selects the sender’s archive for the blind-unicast one-to-one conversation with the requester.
A receiver MUST drop a request containing this flag if the request was not received by blind-unicast or if the option has a non-zero length. The flag has no meaning on other message types and MUST be ignored there.
Sender Handle
A UTF-8 string containing the name or pseudonym of the sender. If not supplied, a handle may be inferred from previously received node metadata for the sender’s address.
Message Sequence
Associates a packet with a monotonically increasing message identifier maintained independently for each sender within each conversation, and optionally carries fragmentation state. The option is encouraged on all messages but is not required. Messages without this option cannot be directly referenced in replies or reactions.
The option value is either 1 byte or 3 bytes:
1-byte form (message ID only):
| 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, each defining its own capabilities, commands, and properties: Radio Control, Frame Transport, Device Domain, Saved State, Tethered Host Services, Wi-Fi, and IP Connectivity
- 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 seven subsystems, each specified by its own chapter and each discoverable through the capabilities it advertises. Only the first two are unconditional.
Radio Control
The host configures the physical radio link, through an interface that is not LoRa-specific in shape where that can be avoided—different radios have different parameter sets. The host can:
- configure frequency, bandwidth, spreading factor, coding rate, power, and similar link parameters where applicable
- query device capabilities and current active configuration
- observe radio health and diagnostics
- observe and limit transmit duty cycle
Frame Transport
The host uses the device as a transport for UMSH frames:
- transmit of raw complete UMSH frames
- receive of raw complete UMSH frames, with RSSI/SNR receive metadata
- transmit result indications
This keeps the layering clean: the ULCP link carries UMSH frames, not re-encoded UMSH semantics. One practical consequence is that the host can also communicate with the device’s own local UMSH node using ordinary UMSH frames sent over this stream, rather than requiring a separate bespoke message path for such traffic.
Device Domain
The radio’s own node identity and the settings that make it behave the way its operator commissioned it: its keys and peers, its name, its repeater switch, what it advertises about itself on the mesh, and its battery telemetry. This is what a host writes when it is administering a device rather than being that device’s host.
Saved State
The device can snapshot its device-domain configuration to non-volatile storage and restore it at boot, which is what lets a radio come back after a power cut still configured and still forwarding with nobody attached.
Tethered Host Services
Receive filtering, inbound queueing, key provisioning, and acknowledgement delegation: the work a device does on behalf of the host identity it is serving.
A major value of a companion radio is letting the host sleep while the device stays awake. For that, the device filters—implicitly from the provisioned host identity and channel keys, explicitly by destination hint, channel identifier, or packet type—so it only wakes the host when a frame is relevant. While the host is disconnected it may also be asked to buffer inbound frames until the host asks for them, and to send MAC acks for peers whose pairwise keys were provisioned.
These remain tightly scoped: the device is assisting the host, not impersonating it in the general case. Outbound traffic is deliberately not queued—a transmit either happens or fails while the host is attached to observe the result.
Wi-Fi
The 802.11 hardware some devices carry alongside the LoRa radio, in three functions the host configures separately: scanning for access points, joining a network as a station, and offering one as an access point. Each is its own capability, because a tracker whose transceiver sniffs beacons for geolocation can scan and can do nothing else.
IP Connectivity
Addressing on whichever link the device has: DHCP or a static configuration per family, the addresses and routers in effect, and the resolvers. One capability per family, and nothing in it knows what the link is.
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.
- A reliable, in-order byte stream standing in for a serial link—a TCP connection to a bridged port, say—uses the same HDLC-Lite framing.
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, CMD_ANNOUNCE in
Device Identity, 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 |
| 16 | CMD_REBOOT | Host->Device | Restart the device’s hardware |
| 19 | CMD_ANNOUNCE | Host->Device | Announce the device now |
| 21 | CMD_PROP_MULTI_GET | Host->Device | Get several property values |
| 22 | CMD_PROP_MULTI_SET | Host->Device | Set several property values in order |
| 23 | CMD_PROP_ARE | Device->Host | Multiple property value notification |
| 24 | CMD_SESSION_RESET | Device->Host | Session state was discarded |
The multi-property commands (21–23) are gated by CAP_CMD_MULTI,
CMD_REBOOT by CAP_REBOOT, and CMD_ANNOUNCE by CAP_ADVERT (see
Capabilities); everything else is unconditional.
CMD 0: (Host -> Device) CMD_NOP
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD_NOP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
^ HEADER ^ COMMAND ^
Figure: Structure of CMD_NOP
No-Operation. Commands the device to reply with a STATUS_OK code. This is
primarily used for liveness checks.
The command payload for this command SHOULD be empty. The receiver MUST ignore any non-empty command payload.
There is no error condition for this command.
CMD 1: (Host -> Device) CMD_RST
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD_RST |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_RST
Reset device. Commands the device to perform a software reset. Due to the nature of
this command, the TID is ignored. The host should instead wait for a
CMD_PROP_IS command from the device indicating PROP_LAST_STATUS has been set
to STATUS_RESET_SOFTWARE (see Status Codes).
The command payload SHOULD be empty, and it SHOULD NOT be processed.
If an error occurs, the value of the emitted PROP_LAST_STATUS will be set
accordingly to the status code for the error.
CMD 2: (Host -> Device) CMD_PROP_GET
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_GET
Get property value. Commands the device to emit a CMD_PROP_IS command for the
given property identifier.
The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers.
If an error occurs, the value of the emitted PROP_LAST_STATUS will be set
accordingly to the status code for the error.
CMD 3: (Host -> Device) CMD_PROP_SET
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NEW PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_SET
Set property value. Commands the device to set the given property to the specific
given value, replacing any previous value, and to emit a CMD_PROP_IS command
for that property indicating the new authoritative value if successful.
The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers, followed by the property value. The exact format of the property value is defined by the property.
If an error occurs, the value of the emitted PROP_LAST_STATUS will be set
accordingly to the status code for the error.
The value reported by that CMD_PROP_IS need not be the value written: a
device that adjusts a write to what it can honor reports the result, and
that result is the property’s value. A write fails only by way of
PROP_LAST_STATUS—a differing value is a successful write, not a
rejected one.
CMD 4: (Host -> Device) CMD_PROP_INSERT
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ITEM VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_INSERT
Insert item into property. Commands the device to add the given item to the
given multi-value property, and to emit a CMD_PROP_INSERTED command for
that property if successful.
The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by exactly one item encoded in the property’s item form (see Multi-Value Properties). The item is not preceded by a length prefix, regardless of whether the property uses item length prefixes in its multi-item value form; the framing layer bounds the item.
If the item is already present the command fails with STATUS_ALREADY,
except where a property defines replacement semantics for matching items
(see, e.g., PROP_HOST_PEER_KEYS). If the property exists but is not a
multi-value property, the command fails with STATUS_INVALID_ARGUMENT.
If an error occurs, the value of the emitted PROP_LAST_STATUS will be set
accordingly to the status code for the error.
CMD 5: (Host -> Device) CMD_PROP_REMOVE
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ITEM SELECTOR ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_REMOVE
Remove item from property. Commands the device to remove the item matching the
given selector from the given multi-value property, and to emit a
CMD_PROP_REMOVED command for that property if successful.
The payload for this command is the property identifier encoded in the packed unsigned integer format, followed by an item selector. Each multi-value property documents its selector form; unless stated otherwise it is the full item value.
If no matching item is present, the command fails with
STATUS_ITEM_NOT_FOUND. If the property exists but is not a multi-value
property, the command fails with STATUS_INVALID_ARGUMENT.
If an error occurs, the value of the emitted PROP_LAST_STATUS will be set
accordingly to the status code for the error.
CMD 6: (Device -> Host) CMD_PROP_IS
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CURRENT PROPERTY VALUE ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_IS
Property value notification. This command can be sent by the device in response to a previous command from the host, or it can be sent by the device in an unsolicited fashion to notify the host of various state changes asynchronously.
The payload for this command is the property identifier encoded in the packed unsigned integer format described in Packed Unsigned Integers, followed by the current value of the given property.
CMD 7: (Device -> Host) CMD_PROP_INSERTED
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_INSERTED
Item-inserted notification. Sent by the device in response to a successful
CMD_PROP_INSERT (with the TID of that command), or unsolicited with a TID
of zero when the device adds an item to a multi-value property for its own
reasons.
The payload is the property identifier followed by the inserted item as the device reports it (see Multi-Value Properties)—never in a form containing key material.
CMD 8: (Device -> Host) CMD_PROP_REMOVED
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI, 1-3 bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| REPORTED ITEM ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_REMOVED
Item-removed notification. Sent by the device in response to a successful
CMD_PROP_REMOVE (with the TID of that command), or unsolicited with a TID
of zero when the device removes an item from a multi-value property for its
own reasons.
The payload is the property identifier followed by the removed item as the device reports it.
CMD 16: (Host -> Device) CMD_REBOOT
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD_REBOOT |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_REBOOT
Restart the device. Commands the device to power-cycle its hardware, keeping every piece of state it has persisted: the saved snapshot, the device identity, the pairing PIN, and all bonds survive, and the device comes back configured as it was.
This differs from CMD_RST, which returns protocol state to
its post-reset values with the device still running, and from
CMD_FACTORY_RESET, which erases
that state before restarting. Between the three, this is the one that
changes nothing—it is how a host clears a condition the protocol cannot
name.
The command payload SHOULD be empty and MUST be ignored. A device that
restarts sends no response: the reboot drops the transport link, and
the TID is therefore irrelevant. A host treats the ensuing disconnect (and
the device’s subsequent reappearance announcing STATUS_RESET_POWER_ON)
as completion, and MUST NOT wait for a PROP_LAST_STATUS.
This command is only available on devices advertising CAP_REBOOT.
A device without it answers STATUS_UNIMPLEMENTED—which is the only
response this command ever produces, and the only thing that distinguishes
a device that declined from one that is already restarting.
CMD 21: (Host -> Device) CMD_PROP_MULTI_GET
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | PROP_KEY (PUI) | PROP_KEY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_MULTI_GET
Get several property values at once. Commands the device to reply with a
single CMD_PROP_ARE carrying one entry for each
requested property, in request order.
The payload is one or more property identifiers, each encoded in the packed unsigned integer format, one after another with no delimiters.
Fetching continues past failures: a property that cannot be fetched
occupies its position in the reply as a PROP_LAST_STATUS entry whose
value is the status code a CMD_PROP_GET of that property would have
produced. Position, not the entry’s key, identifies which request an
entry answers.
This command is available only on devices advertising CAP_CMD_MULTI.
On any other device it is an unrecognized command,
STATUS_INVALID_COMMAND.
CMD 22: (Host -> Device) CMD_PROP_MULTI_SET
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | ENTRY | ENTRY | ENTRY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_MULTI_SET
Set several property values in order. The payload is a sequence of entries, each the combined length of its key and value encoded as a packed unsigned integer, followed by that many octets—the property identifier as a packed unsigned integer, then the value:
+--------------+---------------------------+--------------------+
| LENGTH (PUI) | PROP_KEY (PUI, 1-3 bytes) | VALUE (remainder) |
+--------------+---------------------------+--------------------+
Figure: CMD_PROP_MULTI_SET Entry Format
The device applies each entry exactly as a CMD_PROP_SET of that
property would, strictly in payload order, stopping at the first entry
that fails. Mutation Atomicity applies to each
entry alone: the sequence is not a transaction, and a failure partway
leaves the earlier entries applied.
The reply is a single CMD_PROP_ARE containing, for
each applied entry in order, the property and its reported value—
exactly what the CMD_PROP_IS answering a lone CMD_PROP_SET would
carry—and, for the failing entry, a PROP_LAST_STATUS entry carrying
the error, after which the reply ends. Entries past the failure are not
executed and contribute nothing; a reply whose every entry is a success
covers the entire request.
This command is available only on devices advertising CAP_CMD_MULTI.
On any other device it is an unrecognized command,
STATUS_INVALID_COMMAND.
CMD 23: (Device -> Host) CMD_PROP_ARE
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | ENTRY | ENTRY | ENTRY ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_PROP_ARE
Multiple property value notification. The payload is a sequence of
entries in the same encoding as CMD_PROP_MULTI_SET: a combined
key-and-value length as a packed unsigned integer, the property
identifier, and the value as the device reports it—under the same
reporting rules as CMD_PROP_IS, so key material never appears (see
Multi-Value Properties).
The device emits this command only in response to CMD_PROP_MULTI_GET
or CMD_PROP_MULTI_SET, with the TID of that command. It MUST NOT
be emitted unsolicited: asynchronous updates use CMD_PROP_IS and its
companions, one property at a time.
CMD 24: (Device -> Host) CMD_SESSION_RESET
0 1 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 0| RES | TID | CMD | REASON
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure: Structure of CMD_SESSION_RESET
The device has discarded session state, so every session-scoped property is back at its documented default under a host that did not ask for it. The TID MUST be zero: the frame is always unsolicited.
REASON is a packed unsigned integer. It is diagnostic—a host’s obligation is the same for every value:
| Value | Meaning |
|---|---|
| 0 | A host attached |
| 1 | CMD_RST |
| 2 | CMD_RESTORE in its reset form |
Unassigned values are reserved. A host MUST treat an unrecognized reason as a session reset it does not have a name for, never as a parse failure.
A device MUST emit this command whenever it discards session state while a host is attached, and MUST NOT emit it when no host is attached: a detach discards session state with nobody to tell.
The frame MUST be emitted before any frame belonging to the new
session, so a host can tell which session anything it receives came
from. The completion of the command that caused the discard belongs to
the exchange that requested it and MAY precede the notice; a
CMD_RST is answered by its STATUS_RESET_SOFTWARE and then the
notice. Where a session is instead created by the host’s own first
frame, nothing has been answered yet and the notice comes first.
The command MUST NOT be emitted over the administrative binding. It concerns the local tethered session; an administrator over the mesh is not party to it, and the binding carries only what was asked for.
A host MUST tolerate the frame at any time while attached. On receiving it, a host MUST re-establish anything it holds in session state and SHOULD otherwise resynchronize as it would on a fresh attach (see Attach, Detach, and Synchronization).
This is deliberately not a reset code, and MUST NOT
disturb PROP_LAST_STATUS. The two report different losses:
STATUS_RESET_POWER_ON means the host domain went with the session and
must be reprovisioned in full, whereas a session reset means only session
state went. A reboot implies a session reset, so recording the narrower
fact over the broader one would lose the distinction that governs
recovery.
Properties and Streams
A property is a piece of device state with a value the host can read and, where the property allows it, write. A stream is a packet-like flow that is not modeled as state; streams share the property identifier space and are carried by their own commands (see Frame Transport).
Note
The properties marked as supporting
Ismeans that the property may be emitted asynchronously. All properties that supportGetorSetwill emit anIsto respond with the current/new value of that property.A multi-value property marked as supporting
InsertedorRemovedmay likewise emit those asynchronously, reporting one item the device added or dropped for its own reasons rather than the whole value. A host that tolerates an unsolicitedIsmust tolerate these as well.
Multi-Value Properties
A multi-value property holds an unordered set of items rather than a
single value. PROP_CAPS is one, and is constant; the key, peer, and
filter tables of the device and host domains are mutable ones.
The host writes items (CMD_PROP_SET, CMD_PROP_INSERT) in the
property’s item form. When the device reports items (CMD_PROP_IS,
CMD_PROP_INSERTED, CMD_PROP_REMOVED), it reports them exactly as
written—except where the item form contains symmetric key material. Such
a property documents its reported form instead: the entry with its
key material omitted, or a value derived from it (a channel key is
reported as its derived channel
identifier), so that
secrets can never be read back (see Provisioning
Security).
The commands valid on a mutable multi-value property are:
CMD_PROP_GET—the device replies 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]
The hardware the firmware is running on belongs in
PROP_DEV_MODEL, not here.
PROP 3: PROP_INTERFACE_TYPE
- Type: Single-Value, Constant
- Asynchronous Updates: No
- Required: REQUIRED
- Scope: NLI
- Value Type: PUI
- Units: Enumeration
- Post-Reset Value: Implementation-Specific
This unsigned packed integer identifies the network protocol implemented by this device. It must return the value 8.
PROP 4: PROP_DEV_MODEL
- Type: Single-Value, Constant
- Asynchronous Updates: No
- Required: OPTIONAL
- Scope: Device
- Value Type: STRING
- Post-Reset Value: Implementation-Specific
Contains a zero-terminated ASCII string naming the hardware model the device
is, such as Seeed SenseCAP T1000-E. Where
PROP_DEV_VERSION describes the firmware a device runs,
this describes the thing it runs on, and the two change independently: the
same firmware release covers several models, and a model outlives every
release built for it.
Firmware built for one specific board SHOULD implement this. A device whose hardware has no fixed identity—a simulator, or an implementation that runs on whatever it is compiled for—SHOULD omit the property rather than return an empty or invented string. A host MUST treat a refused get as “this device does not name its hardware” and continue.
The value SHOULD name the product as its vendor does, so that a host can
match it against an external hardware description. It is intended for display
and for lookup, and is deliberately not an identifier: a host that needs to
make decisions based on what a device can do has
PROP_CAPS for that.
PROP 5: PROP_CAPS
- Type: Multiple-Value, Constant
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required: REQUIRED
- Scope: NLI
- Item Type: PUI
- Units: Enumeration
- Post-Reset Value: Implementation-Specific
Describes the supported capabilities of this device. Encoded as a list of packed unsigned integers. See Capabilities for a list of values.
PROP 6: PROP_UPTIME
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required: OPTIONAL
- Scope: Device
- Value Type: UINT32
- Units: Seconds
- Post-Reset Value: 0
Seconds elapsed since the device last booted, truncated toward zero. A device that runs long enough to exhaust the range saturates rather than wrapping, so the value never falsely reports a recent restart.
This dates what PROP_LAST_STATUS describes: the
reset code says why the device last started, and this says how long ago.
Read together they distinguish a node that came up cleanly weeks back
from one that is restarting under a fault and reporting the same code
each time.
Only a power cycle or a genuine restart resets it. CMD_RST returns
protocol state to its post-reset values without rebooting the device, and
MUST NOT reset this property; neither does a host attaching or
detaching.
A device with no monotonic clock to answer from SHOULD omit the property rather than report a fabricated value. A host MUST treat a refused get as “this device does not report its uptime” and continue.
State Classes
Every piece of device state belongs to exactly one of three classes. The classes determine what survives a host attach, a change of host, and a power cycle.
Session State
State that exists only while a host is attached: transaction (TID)
correlation, transport reassembly buffers, and the session-scoped
properties PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL. Session state
is reset to defaults on every attach. Resetting it never affects radio
operation.
Device Domain
State that belongs to the device itself, independent of which host is attached:
- the device identity keypair (independently persisted; never part of the saved snapshot—see Saved State)
- the device identity’s channel keys (
PROP_DEV_CHANNEL_KEYS) and peer list (PROP_DEV_PEERS) - the RF configuration (
PROP_PHY_*), 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.
- TCP—establishment and closure of the connection.
- Bare UART—implementation-defined. A device with no way to detect host presence MAY treat the host as permanently attached, in which case it never enters detached operation and offline assistance (Inbound Queueing, Acknowledgement Delegation) is unavailable on that transport.
On attach, the device MUST reset session state (see State Classes) and
MUST NOT modify the device or host domains in any way. In particular,
the PHY is not disabled and no property outside session state changes
value. The device MUST NOT emit any frame before attach; the first
frame of the session is the CMD_SESSION_RESET
announcing the session state the attach discarded, with reason 0, and
the attach itself produces no other unsolicited notification.
Because attach no longer implies any known default state, the host synchronizes by fetching, not by assuming. The following post-attach procedure is RECOMMENDED:
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.
The same applies to CMD_SESSION_RESET, which
reports the one change no property notification can: the session itself
starting over. A host that receives one runs this procedure again.
On detach, the device discards session state, keeps operating with the current device- and host-domain state, and begins detached operation: accepted frames are queued rather than delivered, and acknowledgement delegation (if enabled) becomes active.
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 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 |
| 22 | STATUS_NOT_PERMITTED |
| 23 | STATUS_CHANNEL_NOT_FOUND |
| 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; or the value written by aCMD_PROP_SETnames an item of another property that does not exist, as a write ofPROP_WIFI_NETWORKnames an entry of the known-network table. It is distinct fromSTATUS_INVALID_ARGUMENTbecause a host acts on it differently: the value is well-formed and the item it names has merely to be created first. STATUS_CURSOR_INVALID- The cursor presented in a Node Management continuation is not one the device can honor—it does not parse, it was issued for a different request, or the underlying data has changed so that the position is meaningless. The administrator restarts the read from an initial, cursor-less request.
STATUS_NOT_PERMITTED- The property or command exists, but the binding the request arrived
over is not allowed to perform it—in particular, a Node
Management write to a property
reserved to the tethered host. Distinct from
STATUS_PROP_NOT_FOUNDandSTATUS_INVALID_COMMAND: the operation would be accepted from a binding with the standing to ask. STATUS_CHANNEL_NOT_FOUND- The request names a channel by its channel
identifier and the
device holds no channel key that derives it. Distinct from
STATUS_ITEM_NOT_FOUND, which concerns an item of the property being written, and fromSTATUS_INVALID_ARGUMENT: the value is well formed, and the channel has only to be provisioned first (PROP_DEV_CHANNEL_KEYS). 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.
A session reset is not one of these. Discarding session state costs the
host what it established there and nothing more, so it is announced by
CMD_SESSION_RESET and leaves PROP_LAST_STATUS
alone. Every reset code above implies a session reset; none of them is
implied by one.
STATUS_RESET_POWER_ON- Cold power-on start.
STATUS_RESET_EXTERNAL- External device reset. This is generally caused by RESET pin on the device being asserted.
STATUS_RESET_SOFTWARE- Software-requested orderly reset. This is generally caused by the host
sending the device
CMD_RST. STATUS_RESET_RESTORED- Protocol reset into the saved snapshot, emitted when a device completes
CMD_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, CAP_CMD_MULTI | 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 |
| 48 | CAP_MAC_BACKHAUL | CAP_REPEATER | Tethered Host Services |
| 49 | CAP_CMD_MULTI | — | Framing and Common Semantics |
| 50 | CAP_BLE | — | BLE Binding |
| 51 | CAP_REBOOT | — | Framing and Common Semantics |
| 52 | CAP_STATS | — | Radio Control |
| 53 | CAP_WIFI_SCAN | — | Wi-Fi |
| 54 | CAP_WIFI | CAP_WIFI_SCAN | Wi-Fi |
| 55 | CAP_IPV4 | — | IP Connectivity |
| 56 | CAP_IPV6 | — | IP Connectivity |
| 57 | CAP_WIFI_AP | CAP_WIFI_SCAN | Wi-Fi |
| 515 | CAP_PHY_LORA | — | Radio Control |
A device MUST NOT advertise a capability without also advertising the
capabilities it requires. Apart from the multi-property commands, which
CAP_CMD_MULTI gates, CMD_REBOOT, which CAP_REBOOT gates, and
CMD_ANNOUNCE, which CAP_ADVERT gates, the
commands and status codes defined in this chapter are unconditional and
need no capability; a device that defines no
mutable multi-value properties simply has nothing to apply
CMD_PROP_INSERT/CMD_PROP_REMOVE to.
ULCP: Radio Control
Radio control is the subsystem the host uses to configure and observe the physical transceiver: frequency, modulation, transmit power, and the transmit duty-cycle budget. Every ULCP device implements it.
The interface is deliberately not LoRa-specific in shape where that can be
avoided. The properties that any radio has—enable, frequency, transmit
power, RSSI, MTU—are unconditional; the LoRa modulation parameters are
gated behind CAP_PHY_LORA, and duty-cycle accounting behind
CAP_PHY_DUTY_LIMIT. Traffic counters are exposed by CAP_STATS.
The RF configuration is device-domain state: it belongs to the radio rather than to whichever host is attached, it is part of a saved snapshot, and it survives a change of host.
Capabilities
| 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 |
| 52 | CAP_STATS | Radio and forwarding traffic counters: PROP_STAT_* |
| 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 |
| 4832 | PROP_STAT_TX_PACKETS | Get, Set | Packets transmitted over the air |
| 4833 | PROP_STAT_TX_CHANNEL_BUSY | Get, Set | Transmissions deferred because the channel was busy |
| 4834 | PROP_STAT_RX_PACKETS | Get, Set | Received packets with a UMSH first-octet pattern |
| 4835 | PROP_STAT_RX_BAD_CRC | Get, Set | Receptions rejected because of a bad CRC |
| 4836 | PROP_STAT_RX_NON_UMSH | Get, Set | Received packets without a UMSH first-octet pattern |
| 4837 | PROP_STAT_RX_ACCEPTED | Get, Set | Received packets accepted by this device’s node |
| 4838 | PROP_STAT_FORWARDED | Get, Set | Packets this device chose to forward |
| 4839 | PROP_STAT_FORWARD_DROPPED | Get, Set | Forwarding candidates rejected by policy |
| 4840 | PROP_STAT_FORWARD_CANCELLED | Get, Set | Queued forwards cancelled after an acknowledgement |
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% |
Statistics
A device advertising CAP_STATS exposes cumulative UINT32_LE traffic
counters. Values count since boot or the most recent reset of that counter and
wrap modulo 2^32. Writing four zero octets resets a counter. A device MUST
reject every other value or length with STATUS_INVALID_ARGUMENT.
Statistics are live hardware history rather than configuration. They are not
part of a saved snapshot, and CMD_RST MUST NOT clear them. A host can reset
several counters with CMD_PROP_MULTI_SET, but the individual resets do not
occur simultaneously: traffic arriving during the write sweep can fall on
different sides of different counter resets.
PROP_PHY_DUTY_NOW is commonly displayed alongside these counters, but it is a
rolling regulatory measurement and is not resettable.
PROP 4832: PROP_STAT_TX_PACKETS
Packets whose transmission completed successfully over the physical radio.
Requires CAP_STATS.
PROP 4833: PROP_STAT_TX_CHANNEL_BUSY
Transmission attempts deferred because channel assessment reported the channel
busy. Such an attempt does not also increment PROP_STAT_TX_PACKETS. Requires
CAP_STATS.
PROP 4834: PROP_STAT_RX_PACKETS
Off-air receptions whose first octet carries the UMSH version and valid reserved
bits. This classification does not require the rest of the packet to parse.
Requires CAP_STATS.
PROP 4835: PROP_STAT_RX_BAD_CRC
Receptions the physical radio rejected because their payload CRC was invalid.
Requires CAP_STATS.
PROP 4836: PROP_STAT_RX_NON_UMSH
Off-air receptions whose first octet does not carry the UMSH version and valid
reserved bits. Requires CAP_STATS.
PROP 4837: PROP_STAT_RX_ACCEPTED
Receptions on which this device’s own node acted. Frames addressed to an
attached host’s identity are not included. Requires CAP_STATS and
CAP_REPEATER.
PROP 4838: PROP_STAT_FORWARDED
Receptions this device’s own node chose to repeat. The counter records the
forwarding decision; successful physical transmissions are counted separately
by PROP_STAT_TX_PACKETS. Requires CAP_STATS and CAP_REPEATER.
PROP 4839: PROP_STAT_FORWARD_DROPPED
Packets that were eligible for forwarding but were rejected by this repeater’s
configured minimum RSSI, minimum SNR, or region policy. Exhausted flood budgets
and packet-imposed signal thresholds are normal packet behavior, not
administrator policy, and are not included. Duplicates, self-addressed packets,
and packets received while forwarding is disabled are also excluded. Requires
CAP_STATS and CAP_REPEATER.
PROP 4840: PROP_STAT_FORWARD_CANCELLED
Queued forwards cancelled because the destination’s acknowledgement was
overheard before transmission. Requires CAP_STATS and CAP_REPEATER.
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
Extended Recv Metadata
The Recv metadata may carry two further trailing fields:
RX_FLAGS(u8): Delivery 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.RX_FLAG_SELF_TXBit 2: The device transmitted this frame itself and is delivering a copy of it.RX_RSSIandRX_SNRMUST carry their unsupported sentinels: a transmitter measures nothing.- All other bits: RESERVED, transmitted as zero
RX_AGE(u32, little-endian): Seconds elapsed between reception of the frame and its delivery to the host. Zero for live delivery.
As with the existing metadata fields, the metadata may be truncated at any field boundary; absent fields are treated as zero. A live delivery with nothing to flag MAY therefore omit these fields entirely, which keeps the encoding byte-compatible with a device that does not queue at all.
ULCP: Device Domain
The device domain is everything that belongs to the radio itself: its own node identity, the settings that make it behave the way its operator commissioned it, and the telemetry it can report about its own hardware. None of it is keyed by the attached host, and none of it is disturbed when one host replaces another (see State Classes).
Commissioning a repeater is exactly this: writing device-domain state and saving it. It is also the reason a host that is merely administering a device writes nothing in the host domain—see Two Kinds of Attach.
The Device Identity
The device hosts a node belonging to the device itself, used for in-band
management, diagnostics, repeater forwarding (see
PROP_MAC_REPEATER_ENABLED),
and (in future revisions) periodic advertisement behavior. Its Ed25519
private key is held by the device and is never readable through this
protocol.
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, PROP_IDENT_LOCATION, PROP_IDENT_ALTITUDE—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) and on demand (CMD_ANNOUNCE) |
| 47 | CAP_ILLUMINANCE | — | An ambient light sensor and PROP_ILLUMINANCE |
CAP_ADVERT requires CAP_DEV_IDENTITY because what an advertisement
carries is the device identity, and a beacon’s source address names
it.
CAP_TIME states that the device keeps a wall clock and nothing else. It
says nothing about where the time comes from, how accurate it is, or how
much of a power cycle it survives—a device that has one and does not
currently know what time it is is a normal state, reported by the empty
PROP_TIME.
CAP_GNSS requires CAP_TIME because a receiver is, among other things,
a clock: a device advertising one without the other would be claiming a
time source for a clock it does not have.
Commands
CMD 19: (Host -> Device) CMD_ANNOUNCE
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+
|1 0| RES | TID | CMD_ANNOUNCE | OPTIONS |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-------------+
Figure: Structure of CMD_ANNOUNCE
Announce the device now, outside either schedule. What the device sends is
an advertisement or a beacon—the same two announcements
PROP_ADVERT_INTERVAL and PROP_BEACON_INTERVAL emit unasked—as a
broadcast or as multicast on one of the device’s own channels.
OPTIONS is a sequence of entries in the
CMD_PROP_MULTI_SET entry form:
LENGTH (PUI) | OPTION (PUI) | VALUE. An empty payload is a
well-formed request carrying every default.
| Option | Name | Value | Default |
|---|---|---|---|
| 1 | ANNOUNCE_KIND | UINT8: 0 advertisement, 1 beacon | 0 |
| 2 | ANNOUNCE_FLOOD_HOPS | UINT8, 0–15 | 0 |
| 3 | ANNOUNCE_FULL_SOURCE | BOOL | 1 for an advertisement, 0 for a beacon |
| 4 | ANNOUNCE_CHANNEL | 16 octets: the full channel identifier | absent: broadcast |
ANNOUNCE_KIND- An advertisement carries the device’s signed node identity; a beacon carries no payload at all.
ANNOUNCE_FLOOD_HOPS- The flood budget the frame goes out with (
FHOPS_REM, see Packet Structure). Zero sends no flood-hop field, reaching only the nodes that hear the device directly. A frame carrying a budget also carries the Trace Route and Trace Signal options, so what arrives at a distant node is a usable path back; a beacon carries both at any budget, a path being what a beacon publishes. ANNOUNCE_FULL_SOURCE- Whether
SRCis the 32-byte public key rather than the 3-byte hint. An advertisement’s detached signature is only checkable against the full key, hence its default; a beacon has nothing to check and defaults to the hint. A host may override either way. ANNOUNCE_CHANNEL- Send as multicast on the device channel whose full
channel identifier this
is—the form that names a channel unambiguously, since two keys can
share a 2-byte
channel_id. It is whatPROP_DEV_CHANNEL_KEYSreports for each key the device holds, so a host names a channel with exactly what it read back. Absent, the announcement is a broadcast. A multicast announcement uses the channel’s encrypted mode.
An option the device does not recognize, a repeated option, or a value of
the wrong length or out of range is STATUS_INVALID_ARGUMENT. A device
MUST NOT silently ignore an unknown option: the host asked for
something specific, and a broadcast sent in place of an ignored multicast
request would be the wrong frame on the air. A malformed entry list is
STATUS_PARSE_ERROR.
The device answers with CMD_PROP_IS of PROP_LAST_STATUS carrying the
command’s TID:
STATUS_OKonce the announcement is queued for transmission. This command reports queuing, not airing: channel access and the duty limit decide later whether the frame reaches the air, exactly as for a scheduled announcement.STATUS_BUSYwhen an announcement is already pending and this one could not be queued behind it.STATUS_CHANNEL_NOT_FOUNDwhenANNOUNCE_CHANNELnames a channel the device holds no key for.STATUS_UNIMPLEMENTEDon a device withoutCAP_ADVERT, or one with no node behind the session to announce.
This command is available on devices advertising CAP_ADVERT. It carries
no tethered-host state, so an administrator may issue it over the
Node Management binding.
Properties
The device domain occupies property identifiers 64–95. Identifiers 70–95 are the device-behavior range: 70–78 are the repeater policy and advertised node identity settings, 79 is the locate alert, 80–87 are what the device announces about itself—80–82 the advertisement schedule, 83–84 the advertised position, 85–87 reserved—88–93 are positioning (88 the receiver switch, 89–93 the fix telemetry), and 94–95 are environmental sensing: 94 illuminance, 95 reserved.
The advertised position at 83–84 and the fix telemetry at 89–90 are deliberately separate properties. The fix is what the receiver currently sees; the advertised position is what the identity claims, which a device without a receiver still has.
A single-octet identifier is the scarce resource, so the positioning range holds the properties a host reads and the device announces continually. The positioning configuration—which is written during commissioning and rarely again—lives at 4868–4870 in the extended device range, alongside the wall clock at 4866–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, Insert, Remove | Region strings 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 |
| 83 | PROP_IDENT_LOCATION | Get, Set | Position the advertised node identity carries |
| 84 | PROP_IDENT_ALTITUDE | Get, Set | Altitude the advertised node identity carries |
| 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)
- Reported Form: 16 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
channel identifier and the
channel’s K_enc/K_mic
(see Multicast Packet Keys). Each
entry is reported as its full 16-octet channel identifier, which is what
names one of these channels to a management interface—two keys can
derive the same 2-byte channel_id, and CMD_ANNOUNCE must not be left
to guess which was meant. The key itself is never read back.
Device channel keys do not create implicit host receive filters: frames on these channels are consumed by the device node and reach the host only through the host’s own filtering.
PROP 67: PROP_DEV_PEERS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_DEV_IDENTITY - Item Form: 32 octets (the peer’s Ed25519 public key)
- Remove Selector: the 32-octet public key
- Post-Reset Value: Empty, or restored from saved state
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: Yes
- Required:
CAP_REPEATER - Value Type: BOOL
- Post-Reset Value: Persisted
The first of the device-behavior settings (property identifiers 70–78),
and the master switch for the repeater policy in
PROP_MAC_REPEATER_REGIONS, PROP_MAC_REPEATER_DEFAULT_REGION,
PROP_MAC_REPEATER_MIN_RSSI, and PROP_MAC_REPEATER_MIN_SNR, which are
configurable while forwarding is disabled and take effect when it is
enabled. When true, the device identity acts as an
autonomous mesh repeater: its on-board node forwards 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.
Asynchronous because a device MAY offer forwarding as a control the operator can reach—the menu on a device with a screen is where a user decides whether to spend their battery carrying other people’s traffic. A device that flips it locally MUST publish the new value like any other transition the host did not command.
Flood-contention tuning—the forwarding delay window, deferral count, and similar timing parameters—is not exposed; a repeater applies its local defaults.
PROP 71: PROP_IDENT
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required:
CAP_IDENT - Value Type: Signed node-identity payload
The device identity’s complete signed node identity: the canonical payload encoding—role, capabilities, and the descriptive options the device advertises— followed by its 64-octet detached EdDSA signature over that encoding.
This is the same statement the device makes over the air, in its standalone framing. A device MUST build it from the same values it would advertise in an Identity Request response, so a host reading it locally and a peer hearing it on the mesh cannot disagree about what the device is. It differs from that response in exactly two ways, both structural: it carries no request nonce, and it is authenticated by the signature rather than by an enclosing authenticated unicast.
The contents are nonce-free 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: Multiple-Value, Read-Write
- Has Item Length Prefix: Yes
- Asynchronous Updates: No
- Required:
CAP_REPEATER - Item Form: 1–24 octets of UTF-8 (the region’s string form)
- Remove Selector: the region string
- Post-Reset Value: Empty, or restored from saved state
The set of regions the device flood-forwards for, each held in its string form—the short code for a region that has one, the region name otherwise—the same form the Supported Regions identity option carries. From each string the device derives the 2-octet region code; the derived codes are the filter applied at the region-policy step of the forwarding procedure: a flood packet carrying region codes is forwarded only if at least one of them matches. An empty set—the factory default—imposes no regional restriction, so a tagged packet is forwarded whatever its region.
A device MUST reject an item that is empty or longer than 24 octets
with STATUS_INVALID_ARGUMENT, and MAY reject a write that exceeds
the number of entries it can hold.
Items compare without ASCII case, as their codes derive, so the set holds
at most one entry per region. An insert whose item differs from a held
one only in case respells that entry in place and reports the item as
inserted; an item identical to a held one reports STATUS_ALREADY. The
remove selector matches the same way, and a whole-value write keeps the
last spelling it carried. Stored items keep the capitalization written.
A device with forwarding enabled and a non-empty set SHOULD advertise the same strings in its node identity, subject to that option’s count and packet-fit limits, so that a peer choosing a route can see what a repeater will carry. A device that is not forwarding makes no such claim and omits the option.
Whether an untagged packet is tagged on the way out is a separate
decision, governed by PROP_MAC_REPEATER_DEFAULT_REGION.
PROP 75: PROP_MAC_REPEATER_DEFAULT_REGION
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_REPEATER - Value Type: One 2-octet region code, or empty
- Post-Reset Value: Empty, or restored from saved state
The region code the device inserts into a flood packet that carries none,
as permitted at the region-policy step of the forwarding
procedure. An empty
value—the factory default—means the device never tags: untagged
packets are forwarded untagged. Any other value MUST be exactly two
octets; a device rejects other lengths with STATUS_INVALID_ARGUMENT.
Tagging is opt-in because it is a claim about where the packet is, not merely about where the repeater is willing to forward. A repeater that filters on a region list without asserting one leaves the decision to whoever originated the packet.
The configured code SHOULD be one of the codes in
PROP_MAC_REPEATER_REGIONS when that list is non-empty, so that the
repeater will itself forward what it tags. A device does not enforce this
across the two writes, and the two properties may be set in either order.
Only untagged packets are affected: an already-tagged packet is forwarded with its codes unchanged, and a second code is never added.
PROP 76: PROP_MAC_REPEATER_MIN_RSSI
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_REPEATER - Value Type: INT16 in dBm, or empty
- Post-Reset Value: Empty, or restored from saved state
The weakest signal the device will flood-forward, in dBm. An empty value—the factory default—imposes no threshold. Any other value MUST be exactly two octets.
The threshold is the repeater’s half of step 7 of the forwarding procedure: where the packet also carries a minimum, the higher of the two applies. Raising it trades reach for a quieter mesh, which is what a dense deployment wants from a repeater sitting at the edge of everyone’s range.
Applies to flood forwarding only. 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.
The schedule is not the only way an announcement goes out.
CMD_ANNOUNCE sends one now, with a reach and a source
form the host chooses; the two are independent, and neither disturbs the
other’s timing.
PROP 81: PROP_BEACON_INTERVAL
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_ADVERT - Value Type: UINT32
- Post-Reset Value: 3600 (one hour), or restored from saved state
Seconds between unsolicited beacons—broadcasts with no payload at all. Zero sends none.
A beacon is sent with a flood budget and 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 83: PROP_IDENT_LOCATION
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_IDENT - Value Type: 0–7 octets
- Post-Reset Value: Empty, or restored from saved state
The position the device identity carries in its node identity, in the variable-precision location encoding. An empty value means the identity advertises no position.
This property is where the advertised position comes from, whatever put
it there. A fix writes it when
PROP_GNSS_IDENT_UPDATE is set;
a host or administrator writes it when that is clear. There is one
advertised position and one place to read it.
Required by CAP_IDENT rather than CAP_GNSS, because where a node is
and whether it can determine that itself are different questions. A fixed
repeater on a hilltop has a position worth advertising and no receiver to
find it with, and a host placing that node needs somewhere to put the
coordinates.
The value’s length is its precision, so a written position needs no
separate precision setting: a host that wants to advertise a
neighborhood rather than an address writes a shorter value.
PROP_GNSS_IDENT_PRECISION
governs what the device clamps its own fixes to and does not constrain
a written value.
A write is refused with STATUS_INVALID_STATE while
PROP_GNSS_IDENT_UPDATE is set.
The device is maintaining the value from its own fixes, and a written
position would survive only until the next one—a setting that silently
reverts is worse than one that refuses. A host that means to place the
node clears auto-update first.
Devices SHOULD announce this property when it changes. Unlike
PROP_GNSS_LOCATION it moves only
when the advertised identity does, which is already rate-limited by the
clamping precision, so announcing it costs what it is worth.
PROP 84: PROP_IDENT_ALTITUDE
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_IDENT - Value Type: 1–4 octets, or empty
- Post-Reset Value: Empty, or restored from saved state
The altitude the device identity carries, in meters above the WGS-84
ellipsoid—the same units and reference as the node identity’s altitude
option. Writable and refused under exactly the conditions described for
PROP_IDENT_LOCATION.
The value is a two’s-complement signed integer, little-endian, in the fewest octets that hold it: an altitude of 100 m occupies one octet, 200 m two, and the range extends to four. Most nodes are within a byte of sea level, and this property is read over a link where a byte is worth saving. Negative values are ordinary—a node below the ellipsoid, which much of the world’s dry land is.
A device MUST accept any length from one through four and sign-extend it, so a host that pads to a fixed width is understood. A device SHOULD report the minimal form.
An empty value means no altitude is advertised. A device that advertises no location advertises no altitude either: an altitude alone places nothing.
PROP 88: PROP_GNSS_ENABLED
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_GNSS - Value Type: BOOL
- Post-Reset Value: device-defined, conventionally 0 (false), or restored from saved state
Whether the GNSS receiver is powered.
Asynchronous because a device MAY offer the receiver as a control the operator can reach—a button, a menu entry—and a switch someone can flip is a value that moves without the host asking. A device that flips it locally MUST publish the new value like any other transition the host did not command.
False means the lowest power state the board can put the receiver in, not merely an idle one: on a battery-powered node the receiver is typically the largest continuous load there is, and a property that only stopped reporting would be a property that solved nothing.
The post-reset value is the device’s to choose, and it SHOULD be false: a device that has never been told to care where it is should not be spending a battery finding out. A device whose purpose is to know where it is—a fixed outdoor node with a panel rather than a pocket tracker on a cell—MAY default it true instead, and SHOULD document that it does. Either way the value is only a starting point: saved state overrides it in both directions, and a host that wants a particular state sets it rather than assuming one.
Disabling the receiver does not clear the wall clock. Time already obtained stays as good as the device’s oscillator keeps it, which is the whole point of having acquired it.
One exception is permitted, and only for boards where the receiver’s own
real-time-clock domain is the only clock the board has: that domain
MAY remain powered while this property is false, and the device
MAY briefly power the receiver at boot to read the time back out of
it. That is a clock operation, not a positioning one—position data
observed during it is discarded, and it is governed by
PROP_GNSS_TIME_TRUST rather than
by this property.
PROP 89: PROP_GNSS_LOCATION
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_GNSS - Value Type: 0–7 octets
- Post-Reset Value: Empty
The position of the most recent fix, in the variable-precision location encoding—the same nibble-interleaved grid code node identities carry, so a host never has to convert between two position formats.
An empty value means no fix has been obtained since the receiver was last powered. The device reports the position it actually has rather than the last one it remembers across a power cycle: a position that was true somewhere else is worse than no position at all.
The length is the device’s own honest precision for that fix and MAY vary between reads. A host MUST NOT read more precision into a value than its length carries; the encoding’s truncation property means a shorter value is a correct lower-precision statement of the same position, never a different one.
A device MUST generally avoid announcing this property, and a host that wants a position MUST be prepared to read one. A receiver produces a fix about once a second, and at fine precision the readings of a receiver standing perfectly still still differ from each other, so a device that published every change would transmit continuously—and wake its host every time—on behalf of a host that may not be looking. No threshold rescues this: the one that would be quiet enough to be worth having is coarse enough that the announcements it does send are too late to be the point.
A host SHOULD nonetheless accept an announcement that arrives. The value carries what a read would have returned, a device may have its own reason to volunteer one, and a host that treats it as a protocol error gains nothing for the strictness.
Read-only. This property reports what the receiver sees, and nothing a
host writes could make that true. A position placed by hand belongs to
PROP_IDENT_LOCATION, which is
what the identity advertises and is writable for exactly that reason.
PROP 90: PROP_GNSS_ALTITUDE
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required:
CAP_GNSS - Value Type:
INT32_LE, or empty - Post-Reset Value: Empty
Altitude of the most recent fix, in meters, in the same units and reference as the node identity’s altitude option, so the two are directly interchangeable.
Empty when there is no three-dimensional fix—including while the
receiver holds a two-dimensional one, which has a position but no
altitude. Read-only for the same reason as
PROP_GNSS_LOCATION.
PROP 91: PROP_GNSS_FIX
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_GNSS - Value Type:
UINT8 - Post-Reset Value: 0
The quality of the current position solution.
| 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: Yes
- Required:
CAP_GNSS - Value Type: BOOL
- Post-Reset Value: 0 (false), or restored from saved state
Whether position fixes refresh the location the device identity advertises in its node identity.
Off by default. Broadcasting where you are is a decision, and a device that started doing it because a receiver was switched on would be making that decision on the operator’s behalf.
When on, the device clamps each fix to
PROP_GNSS_IDENT_PRECISION
before advertising it. A device SHOULD act on a fix only when the
clamped position actually changes, rather than on every fix: at a coarse
precision a stationary node’s fixes all land in the same cell, and
re-advertising each one spends airtime to say nothing.
Asynchronous because a device MAY offer this as a control the operator can reach, alongside the receiver switch it is deliberately separate from. A device that flips it locally MUST publish the new value like any other transition the host did not command.
Switching it off stops the updating and leaves
PROP_IDENT_LOCATION and
PROP_IDENT_ALTITUDE holding the
last position written to them, which becomes an ordinary written value
that a host may then change or clear. This is how a fixed node is
placed without anyone reading coordinates off a screen: switch the
receiver on, let it find where it is, switch auto-update off, and what
it found stays.
The value is therefore a claim the operator is making rather than one the device is maintaining, and a node that moves afterward advertises where it was. Clearing the location is the way to stop advertising a position; a host that wants no stale claim writes an empty value.
Note that the Unix Timestamp option dates the identity payload, not the position—a node that has been stationary for a day still stamps each payload with the moment it was built.
PROP 4869: PROP_GNSS_IDENT_PRECISION
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_GNSS - Value Type:
UINT8 - Post-Reset Value: 5, or restored from saved state
How many octets of variable-precision location the advertised position is clamped to, 1 (coarsest) through 7 (finest).
The default of 5 is a cell of roughly 38 × 19 m at the equator: fine
enough to place a node on a street, coarse enough not to place it in a
room. That trade—not the receiver’s accuracy—is what this property
exists to control, which is why it is separate from
PROP_GNSS_PRECISION.
0 is rejected with STATUS_INVALID_ARGUMENT rather than read as
“advertise nothing”:
PROP_GNSS_IDENT_UPDATE is how
the advertisement is switched off, and a precision that silently meant
the opposite of a precision would be a trap. Values above 7 are rejected
for the same reason the encoding stops there.
Writable while auto-update is off, so a whole positioning policy can be staged and enabled last.
PROP 4870: PROP_GNSS_TIME_TRUST
- Type: Single-Value, Read-Write
- Asynchronous Updates: 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.Traffic statistics (
PROP_STAT_*) are also excluded. They are live hardware history since boot or the last explicit counter reset, not configuration a snapshot can restore.PROP_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.What a network link is currently doing is excluded on the statistics’ reasoning.
PROP_WIFI_SCANNING,PROP_WIFI_SCAN_RESULTS,PROP_WIFI_LINK,PROP_WIFI_RSSI,PROP_WIFI_AP_STATE,PROP_WIFI_AP_CLIENTS, and every IP property but the two configurations andPROP_IP_DNSdescribe what the device found in front of it, not what its operator asked for. A restored lease or a restored association would be a claim about a network the device may not be near. The Wi-Fi configuration that produces them, credentials included, is saved, which is what lets a device rejoin unattended. -
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, filter table, and mute tables are cleared, PROP_HOST_AUTO_ACK
reverts to false, and the inbound queue is discarded. Because the host
domain is never persisted, this is a live-state operation with no durable component:
a power cycle cannot resurrect a previous host’s provisioning regardless.
Setting PROP_HOST_KEY to its current value is idempotent and has no side
effects.
This rule is what makes re-pairing safe: when a companion radio is paired with a different phone, the new host configures its own identity and the previous host’s keys, filters, and queued traffic cease to exist—while the device domain (the radio’s own identity, channels, and settings) is untouched.
Capabilities
| 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, the buffered-frame metadata, and the mute tables |
| 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 |
| 48 | CAP_MAC_BACKHAUL | CAP_REPEATER | PROP_MAC_BACKHAUL and the point-to-point link to the device’s node |
A device advertising none of these is a transparent radio: it delivers every frame it receives and holds nothing on anyone’s behalf.
Commands
| 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. Two properties sit
outside that range: PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL govern
how the attached session sees the radio rather than how the device is
provisioned, and are the protocol’s session-scoped properties.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 48 | PROP_MAC_PROMISCUOUS | Get, Set | Deliver all frames (session-scoped) |
| 50 | PROP_MAC_BACKHAUL | Get, Set | Point-to-point link to the device’s node (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 |
| 104 | PROP_HOST_MUTED_CHANNELS | Get, Set, Insert, Remove | Channels whose queued frames raise no cue |
| 105 | PROP_HOST_MUTED_PEERS | Get, Set, Insert, Remove | Peers whose queued frames raise no cue |
PROP 48: PROP_MAC_PROMISCUOUS
- Type: Single-Value, Read-Write, Session-Scoped
- Asynchronous Updates: No
- Required:
CAP_HOST_FILTER - Value Type: BOOL
- Post-Attach Value: 0 (false)
When true, every frame the PHY successfully receives is delivered to the
host over STR_PHY_RAW, bypassing receive filtering. This is a live-session
diagnostic mode: frames that are delivered only because of promiscuous
mode are never queued while the host is detached, and never acknowledged on
the host’s behalf.
Session-scoped: it reverts to false on every attach.
PROP 50: PROP_MAC_BACKHAUL
- Type: Single-Value, Read-Write, Session-Scoped
- Asynchronous Updates: No
- Required:
CAP_MAC_BACKHAUL - Value Type: BOOL
- Post-Attach Value: 0 (false)
When false, the host and the device’s own node are two listeners on one shared medium. Both transmit through the same radio and both hear what it receives, so a frame from one reaches the other only by way of some third node that repeats it.
When true, the host is instead a point-to-point neighbor of the device’s node:
- A frame the host sends on
STR_PHY_RAWis delivered to the node as though the node had heard it, and is never transmitted directly. It spends no airtime and is not subject to the duty-cycle limit. - Frames the node transmits are delivered to the host with
RX_FLAG_SELF_TXset, subject to the usual receive filtering. - Frames the radio receives are not delivered to the host at all. The node is the only thing listening to the medium.
Traffic between the host and the mesh therefore crosses the device’s repeater, which is what makes the arrangement useful to an internet bridge: hop accounting, duplicate suppression, and forwarding policy are the node’s, applied to the host’s traffic as to anyone else’s. A device whose repeater is disabled still delivers the host’s frames to its own identities, but carries nothing onward.
PROP_MAC_PROMISCUOUS composes with this: it removes receive filtering
from what the host is delivered, which in this mode is the node’s
transmissions.
Session-scoped: it reverts to false on every attach.
PROP 96: PROP_HOST_KEY
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_HOST_FILTER - Value Type: 32 octets, or empty
- Post-Reset Value: Empty
The Ed25519 public key of the tethered host identity. Setting this property tells the device which node identity it is assisting; an empty value means no host identity is configured.
Setting this property to a value different from its current value resets the entire host domain, as specified in Host Replacement. Setting it to its current value is idempotent.
Like the rest of the host domain, this property is never saved: it is empty at every power-on, whatever the radio was doing before.
A configured host key acts as an implicit destination-hint receive filter (see Receive Filtering).
PROP 97: PROP_HOST_CHANNEL_KEYS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_HOST_KEYS - Item Form: 32 octets (the channel key)
- Reported Form: 16 octets (the derived channel identifier)
- Remove Selector: the 32-octet channel key
- Post-Reset Value: Empty
The set of channel keys provisioned
for the host identity. For each key the device derives the
channel identifier and
the channel K_enc/K_mic. Each entry is reported as its full 16-octet
channel identifier, which is the width at which two channels can be told
apart; the key itself is never read back.
The identifier’s leading 2 octets are the channel_id the CHANNEL field
carries, and they act as an implicit channel receive filter
(see Receive Filtering). Host channel keys serve two assistance
purposes:
- recognizing multicast traffic on the host’s channels while the host is detached, so it can be queued; and
- recognizing blind unicast traffic addressed to the host identity, which requires the channel key to decrypt the concealed destination/source addresses (see Blind Unicast Processing) and to form the combined blind unicast payload keys used for authentication and acknowledgement.
Channel keys are group-membership credentials, not host private keys, so provisioning them is consistent with the security boundary. They still grant whoever holds the device the ability to read and send traffic on those channels; see Provisioning Security.
PROP 98: PROP_HOST_PEER_KEYS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_HOST_KEYS - Item Form: Structure, 96 octets
- Reported Form: 32 octets (the peer’s public key)
- Remove Selector: the 32-octet peer public key
- Post-Reset Value: Empty
Pairwise symmetric key material provisioned for specific already-known peers of the host identity. The item form is:
+---------------------+-----------+-----------+
| PEER_PUBLIC_KEY | K_ENC | K_MIC |
+---------------------+-----------+-----------+
32 B 32 B 32 B
Figure: Peer key entry item form
Where PEER_PUBLIC_KEY is the peer’s Ed25519 public key and K_ENC and
K_MIC are the stable pairwise keys for the (host, peer) pair, derived by
the host as described in
HKDF Inputs for Unicast. The device
never derives these itself—it cannot, because it does not hold the host’s
private key.
As an exception to the usual CMD_PROP_INSERT duplicate rule, inserting an
entry whose PEER_PUBLIC_KEY matches an existing entry replaces that
entry. Replacement updates only the stored key material: the peer’s replay
baseline (see Acknowledgement Delegation) and any frames already queued from that
peer are unaffected, since both are keyed by the peer’s identity rather
than by the key values. An entry is reported as its peer public key
alone: K_ENC and K_MIC are never read back.
Provisioned peer keys let the device authenticate inbound unicast and blind unicast from those specific peers and acknowledge it on the host’s behalf (see Acknowledgement Delegation). They grant no capability regarding any other peer, and do not allow the device to establish new pairwise relationships.
PROP 99: PROP_HOST_RX_FILTERS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: Yes
- Asynchronous Updates: No
- Required:
CAP_HOST_FILTER - Item Form: Structure
- Remove Selector: the full item
- Post-Reset Value: Empty
The explicit receive filter table. Each item is a filter entry:
+-------------+----------------------+
| FILTER_TYPE | FILTER_VALUE ...
+-------------+----------------------+
1 B type-specific
Figure: Filter entry format
| 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.
PROP 104: PROP_HOST_MUTED_CHANNELS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_HOST_RX_QUEUE - Item Form: 16 octets (the full channel identifier)
- Remove Selector: the item
- Post-Reset Value: Empty
The channels whose queued frames raise no receipt cue. A frame matches when the provisioned channel key that authenticated it derives a listed identifier; the match is made after verification, against the full identifier, never against the 2-octet on-wire one, so an identifier collision cannot mute a channel the host did not name.
PROP 105: PROP_HOST_MUTED_PEERS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_HOST_RX_QUEUE - Item Form: 32 octets (a peer public key)
- Remove Selector: the item
- Post-Reset Value: Empty
The peers whose queued frames raise no receipt cue. A frame matches when its resolved source is a listed key. Items are full public keys rather than 3-byte hints: hints collide, and the detached receive path resolves the full key already.
Receipt Cues
A device may indicate locally that it took a frame in for a host that is not attached—a sound, a light, a count on a panel. What form the indication takes, and whether a device makes one at all, is a property of the board rather than of the protocol.
PROP_HOST_MUTED_CHANNELS and PROP_HOST_MUTED_PEERS name sources whose
frames are taken in without one. They govern the indication and nothing
else: a frame from a muted source is filtered, queued, acknowledged, and
drained exactly as any other, and counts toward
PROP_HOST_RX_QUEUE_COUNT and PROP_HOST_RX_QUEUE_DROPPED the same way.
Indications that describe the queue as a whole rather than one arrival—a
queued count, a “something is waiting” light—still follow every frame,
because they describe what a drain will deliver.
The tables are not validated against the key tables. An entry naming a
source the device holds no key for is simply inert, as an entry in
PROP_HOST_RX_FILTERS is independent of the key tables. A frame accepted
by an explicit filter alone has no resolved source and therefore matches
no entry.
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.
Directed traffic that asks for no acknowledgement follows the same shape.
A UNIC or BUNI frame accepted only by an implicit filter is delivered
live but MUST NOT be queued; queueing it requires an explicit
FILTER_PKT_TYPE entry selecting that packet type. Its sender is not
waiting on the device—nothing about it will be repeated or given up on—
so the frame is a request whose asker has moved on by the time a drain
runs, while the queue slot it takes is one an ack-requesting frame needed.
The rule is stated as a default the host must override rather than one it
can refuse, because filters only widen: an entry for UNAR would add to
what the implicit destination-hint filter already admits, never narrow it.
Device-domain state never creates implicit host filters: frames for the device identity or its channels reach the host only if the host’s own filtering matches them.
Compatibility rule: when no host key is configured, no host channel keys are provisioned, and the explicit filter table is empty, filtering is considered unconfigured and every successfully received frame is accepted for live delivery. This is exactly how a device with no host services at all behaves, so a host using the radio as a plain frame pipe observes no difference on a filtering device in its factory state. As soon as any filter (implicit or explicit) exists, only matching frames are accepted.
Like the broadcast rule, this one is live-only. A device with filtering unconfigured queues nothing while detached: the host domain does not survive a power cycle, so every device passes through this state on the way from power-on to its host’s first write, and a device that queued here would fill its queue with whatever was in earshot—its own transmissions included—on nobody’s behalf.
A device MUST NOT queue a frame it transmitted itself. The copy a
device delivers of its own transmission (RX_FLAG_SELF_TX) lets an
attached host’s MAC observe its frame reaching the air; replayed from the
queue it describes a transmission the host completed long before.
Promiscuous mode (see PROP_MAC_PROMISCUOUS) bypasses filtering for live
delivery only.
Inbound Queueing
When CAP_HOST_RX_QUEUE is supported and the host is detached,
accepted frames are placed in a FIFO inbound queue instead of being
discarded. Each queue entry records the frame, its receive metadata (RSSI,
LQI, SNR), the time of reception, and whether the device acknowledged it (see
Acknowledgement Delegation).
When the host is attached, accepted frames are delivered live over
STR_PHY_RAW as they always are. Attaching does not flush
the queue: frames queued while the host was away remain queued until the
host issues CMD_QUEUE_DRAIN. Frames received
after attach are therefore delivered live even while older frames remain
queued, and live deliveries MAY interleave with buffered deliveries during
a drain (RX_FLAG_BUFFERED distinguishes them). A host that wants to
process the backlog first drains promptly after attaching and MAY defer
its processing of interleaved live deliveries; RX_AGE in the
buffered-frame metadata gives coarse (one-second) relative timing but is
not sufficient to reconstruct a strict total order—and UMSH itself does
not guarantee in-order delivery in any case.
The queue is circular: when a new frame is accepted and the queue is
full, the oldest queued frame is discarded and the new frame is appended.
The queue therefore always holds the most recent accepted traffic. Every
frame discarded by this eviction increments
PROP_HOST_RX_QUEUE_DROPPED.
Eviction can discard a frame that was already acknowledged on the host’s behalf—the sender believes it delivered, but the host will never receive it. This is the same best-effort custody semantic that applies to power loss (see Acknowledgement Delegation and Saved State): a delegated ack asserts volatile custody, not guaranteed delivery.
Duplicate detection for queueing uses the standard final-destination mechanisms of replay detection: per-peer frame-counter state and the recent accepted-MIC cache used for the backward window, where the device holds the keys to apply them. A frame identified as a previously accepted frame MUST NOT consume an additional queue slot; it is coalesced with the existing entry. A Route Retry form of a queued frame is the same logical packet (same MIC and frame counter) and coalesces with it. Coalescing a duplicate is separate from acknowledging it—a coalesced duplicate may still have its ack retransmitted under the duplicate-acknowledgement window (see Acknowledgement Delegation). For frames the device cannot authenticate (no provisioned keys), no protocol-defined duplicate detection applies and each received frame occupies its own entry.
Acknowledgement Delegation
With PROP_HOST_AUTO_ACK enabled, the device acknowledges qualifying inbound
frames so that senders’ retransmission logic is satisfied while the host is
away. The device MUST transmit a MAC ack for a received frame if and only
if all of the following hold:
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. It is routed from what the acknowledged
frame itself teaches, the way a host MAC routes an ack from the route that
frame just taught it (see Route Learning); the
device holds no other routing state for the host’s peers:
- A frame carrying a trace-route option is acknowledged down that trace as the ack’s source route; the trace is accumulated most-recent first, so it already reads in return order. An empty trace is a direct neighbor and gets a direct ack.
- A frame carrying no trace but a flood hop count gets
FHOPS_REMinitialized from itsFHOPS_ACC, with any region-code options replayed. - A frame carrying a source-route option and no trace—including an
emptied option, which the last repeater keeps for provenance—spent flood
hops only past the route’s end, so its
FHOPS_ACCis not a distance. The ack floods at a default budget of 5 flood hops, or atFHOPS_ACCif that is larger. - A frame with neither option and no flood hop count was heard off the sender’s own transmitter and is acknowledged directly.
An ack a repeater may carry mirrors the frame’s trace-route request, so the sender learns the return path from the only frame an ack-only exchange gives it.
Delegated ack transmissions use the device’s normal transmit path and are subject to the configured duty-cycle limit; the device MUST NOT exceed the limit to send an ack. An ack that cannot be sent leaves the queued frame marked unacknowledged.
Frames that are accepted but fail any of conditions 2–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: Wi-Fi
Wi-Fi control is the subsystem the host uses to configure and observe a device’s 802.11 hardware. It covers three functions, each its own capability, because the hardware that has one does not always have the others:
- Scanning, listening for access points and reporting what was heard. Every Wi-Fi receiver can do this, including ones that can do nothing else.
- The station, joining a network the device is given and staying on it.
- The access point, offering a network of the device’s own for other stations to join.
This chapter specifies the interface, not what the connection carries. Addressing is IP Connectivity; a bridge tunnel, a time source, or a binding a client could attach through is a capability of its own.
Everything here is device-domain state.
The configuration properties, the two switches, the network table, the
selection, and the access point’s network, are part of a saved
snapshot and survive a change of
host. The rest is live: what a scan found, what the link is doing, who
is on the access point. Live state is never saved, and CMD_RST
reaches it only through the configuration it reverts, so each live
property’s post-reset value is declared as whatever the fact is at the
time.
A device with a station and a network selected tries to be on that network whenever it is enabled, with no host present and none ever required. That is why nothing here is a command: a device on Wi-Fi is infrastructure, and infrastructure that needs a phone to get back on the network after a power cut is not.
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 53 | CAP_WIFI_SCAN | — | A Wi-Fi receiver the device can scan with: PROP_WIFI_SCANNING, PROP_WIFI_SCAN_RESULTS |
| 54 | CAP_WIFI | CAP_WIFI_SCAN | A Wi-Fi station the device can enable and join networks with: PROP_WIFI_ENABLED, PROP_WIFI_NETWORKS, PROP_WIFI_NETWORK, PROP_WIFI_LINK |
| 57 | CAP_WIFI_AP | CAP_WIFI_SCAN | An access point the device can bring up: PROP_WIFI_AP_ENABLED, PROP_WIFI_AP_CONFIG, PROP_WIFI_AP_STATE, PROP_WIFI_AP_CLIENTS |
Three capabilities on one base, and everything else discovered by
asking. The scan is the base because it is what every Wi-Fi radio can
do: a station that can join can always scan, and so can a radio that
can beacon, so both requiring CAP_WIFI_SCAN is a fact about hardware
rather than a policy, and the rule that a device
advertises what its
capabilities require does the rest.
The station and the access point do not require each other. Nearly every chip does both, but they are different functions with different state, and a device that has one and not the other is describable. A device advertising any of the three MUST serve every property it grants.
The two remaining station properties, PROP_WIFI_RSSI and
PROP_WIFI_MAC, report things a stack may not expose, and a device
that only scans has neither. A device that cannot answer them answers
STATUS_PROP_NOT_FOUND, in the same exchange the host was already
making, for the reason the BLE binding
gives: a refusal is a complete answer, and a second capability buys a
host nothing it cannot learn in the reply it is already waiting for.
None of the three requires CAP_SAVE. Without it the configuration is
volatile and the device knows no networks after a power cycle, which is
a worse device but a conforming one.
Properties
Allocated from the extended device and transport configuration range: the station and its scan in a block of sixteen after the BLE transport’s, the access point in a block after IP Connectivity, so that neither the station nor the stack has to move to make room.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 4880 | PROP_WIFI_ENABLED | Get, Set, Is | Whether the station is up |
| 4881 | PROP_WIFI_NETWORKS | Get, Set, Insert, Remove | Known networks and their credentials |
| 4882 | PROP_WIFI_NETWORK | Get, Set, Is | The selected network, or empty |
| 4883 | PROP_WIFI_SCANNING | Get, Set, Is | Whether a scan is in progress |
| 4884 | PROP_WIFI_SCAN_RESULTS | Get, Is, Inserted | What the current or last scan has found |
| 4885 | PROP_WIFI_LINK | Get, Is | Link state, failure reason, and association |
| 4886 | PROP_WIFI_RSSI | Get | Signal of the current association |
| 4887 | PROP_WIFI_MAC | Get | The station’s MAC address |
| 4912 | PROP_WIFI_AP_ENABLED | Get, Set, Is | Whether the access point is up |
| 4913 | PROP_WIFI_AP_CONFIG | Get, Set | The network the device offers |
| 4914 | PROP_WIFI_AP_STATE | Get, Is | Whether it is beaconing, and where |
| 4915 | PROP_WIFI_AP_CLIENTS | Get, Is, Inserted, Removed | Who is on it |
4888 through 4895 are reserved for the station, and 4916 through 4927 for the access point.
PROP 4880: PROP_WIFI_ENABLED
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_WIFI - Value Type: BOOL
- Post-Reset Value: 0 (false), or restored from saved state
Whether the station is up. Cleared, the device MUST drop any
association, abandon any scan in progress, and put the station into the
lowest power state the platform offers. This is PROP_GNSS_ENABLED’s
promise rather than PROP_BLE_ENABLED’s: on a battery-powered node a
Wi-Fi radio that is merely idle is still the largest load on the board,
and a property that only stopped reporting would solve nothing.
Set again, the device comes back up and, if a network is selected, starts joining it. The known-network table and the selection are configuration and are not disturbed in either direction, so turning the station off and on is not a way to forget anything.
Off by default, for the reason PROP_GNSS_ENABLED is: a device that
has never been given a network has nothing to spend the power on. Saved
state overrides the default, which is how a commissioned device comes
up connected.
Asynchronous because a device with a screen MAY offer the switch on it, and a switch someone can flip is a value that moves without the host asking.
A write of 1 answers STATUS_INVALID_STATE on a platform that cannot
run the station alongside another radio it currently has on, the
device’s own access point included. The device MUST NOT take the
other radio down to honor the write; the host turns that one off first.
A platform that coexists, which is most of them, answers the write like
any other.
PROP 4881: PROP_WIFI_NETWORKS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: Yes
- Asynchronous Updates: No
- Required:
CAP_WIFI - Item Form: structure below
- Reported Form: the item with its credential omitted
- Remove Selector: the SSID
- Post-Reset Value: Empty, or restored from saved state
The networks the device knows. Each item:
+-------+----------+----------+----------+------------------+
| FLAGS | SECURITY | SSID_LEN | SSID | CREDENTIAL ... |
+-------+----------+----------+----------+------------------+
1 B 1 B 1 B 1-32 B security-defined
Figure: Known-network item format
FLAGS bit 0 set means the network hides its SSID and the device probes for it by name rather than waiting to hear it. Bit 1 set means the credential is a raw 32-octet pairwise master key rather than a passphrase, which is valid only for the modes that derive one; a host that holds the key need never hand the device the passphrase, and a device never has to run the derivation. Bits 2 through 7 are reserved and MUST be zero.
SECURITY is one mode:
| Value | Name | Credential |
|---|---|---|
| 0 | WIFI_SEC_OPEN | none |
| 1 | WIFI_SEC_OWE | none |
| 2 | WIFI_SEC_WPA2 | passphrase of 8-63 octets, or a key |
| 3 | WIFI_SEC_WPA3 | password of 1-128 octets |
| 4 | WIFI_SEC_WPA | passphrase of 8-63 octets, or a key |
| 5 | WIFI_SEC_WEP | none defined |
| 6 | WIFI_SEC_WPA2_ENT | none defined |
| 7 | WIFI_SEC_WPA3_ENT | none defined |
| 8 | WIFI_SEC_WPA3_ENT_192 | none defined |
Values 9 through 15 are reserved. Sixteen is the ceiling because
PROP_WIFI_SCAN_RESULTS reports these as a
16-bit set.
WIFI_SEC_OPEN is no security at all. WIFI_SEC_OWE is Enhanced Open:
encrypted against anyone listening, authenticated against nobody, with
no credential, and the only credential-free mode permitted on 6 GHz.
An OWE entry names the network as the operator sees it. Where that is a
plain OWE network the device joins it; where it is the visible half of
an OWE transition deployment, the device follows the transition element
to the hidden companion BSS and joins that. PROP_WIFI_LINK then
reports the companion’s BSSID and frequency, which is all it ever
reports of any association, while PROP_WIFI_NETWORK goes on naming
the visible network the operator selected. The entry never names the
companion, because the operator never saw it.
WIFI_SEC_WPA2 is WPA2-Personal and covers the WPA/WPA2 mixed networks
that negotiate CCMP; WIFI_SEC_WPA is the TKIP-only remainder, which a
device MAY decline to join. WIFI_SEC_WPA3 is WPA3-Personal, SAE,
whose password is not a WPA2 passphrase: SAE puts no bounds on it, so
the 8 to 63 rule does not apply, and the table admits up to 128 octets,
which is where the more generous stacks stop. A device whose stack
holds a shorter limit refuses a longer password with
STATUS_UNIMPLEMENTED, since the entry is well-formed and the device
is what cannot hold it. A password shared with a transition network’s
WPA2 side is 8 to 63 octets by that side’s rule, which is the host’s to
know.
The remaining four exist so that scan results can say what they heard. WEP is not worth a credential form, and the three enterprise modes are a provisioning surface this chapter does not open; they are numbered so that opening it later renumbers nothing.
A passphrase or password is the UTF-8 encoding of what the operator
typed, with no terminator and no U+0000, and its length bounds count
octets. The device MUST derive keys from exactly those octets: the
PBKDF2 of 802.11 Annex J for WIFI_SEC_WPA2 and WIFI_SEC_WPA, and
SAE over the octets directly. An ASCII passphrase therefore yields what
every router yields, and a non-ASCII one yields what a router that
accepted UTF-8 yielded. In both cases the host’s job is to hand over
the same octets the router’s operator entered, and the device’s is not
to reinterpret them. A raw key, FLAGS bit 1, is exactly 32 octets and
skips the derivation.
An insert is refused with STATUS_INVALID_ARGUMENT when it is not
well-formed: an empty SSID, a mode with no credential form, a
credential of the wrong length or kind for its mode, a reserved mode, a
reserved flag bit set. The SSID is never empty because an empty
PROP_WIFI_NETWORK means no selection, so an entry with no name could
be stored and never chosen; a hidden network has a real name, and it is
only the advertisement that is blank. An insert is refused with
STATUS_UNIMPLEMENTED when it is well-formed and this device cannot do
it: a WPA3 entry on a chip without SAE, or a WPA entry on a device that
declines TKIP. The two are different answers because a host acts on
them differently. The first is a bug in the host, and the second is a
reason to pick the next mode the network offers.
The security mode is exact. It is the mode the device uses, not a
ceiling it negotiates down from. An entry marked WIFI_SEC_WPA3 joins
a WPA2/WPA3 transition network with SAE and fails against a WPA2-only
one; an entry marked WIFI_SEC_WPA2 joins either with PSK; an entry
marked WIFI_SEC_OWE never falls back to open. A device MUST NOT
negotiate a mode other than the one the entry names, so a host that
takes the mode out of a scan result gets exactly the network it saw,
and an evil twin advertising a weaker one gets nothing. Where the
passphrase is the same across a transition network’s modes, which it
usually is, the host writes the strongest one the device accepts.
Items are keyed by SSID: the table holds at most one entry per network,
and an insert whose SSID matches an existing entry replaces it and
reports the item as inserted. That is the path for a wrong passphrase,
a changed one, or a mode upgrade, and it never needs a remove. The
SSID is octets, not text, and a device compares it bytewise. A
whole-table CMD_PROP_SET carrying two entries with the same SSID is
refused with STATUS_INVALID_ARGUMENT before anything changes, under
Mutation Atomicity: the value is an
unordered set, so neither entry has standing to win, and a host that
wrote both did not mean either.
The credential is write-only, under the rules of Provisioning
Security. The reported form is the
item through its SSID, and neither CMD_PROP_GET nor any notification
ever carries a passphrase. This is why replacing an entry is the only
way to change its credential: a host cannot read one back to compare
it. Writes that carry a credential are subject to the same transport
requirement as key material.
A device bounds the table and refuses an insert past its capacity with
STATUS_NOMEM. Four entries is enough for anything a device of this
class does, and a device SHOULD hold at least that many. The bound
MUST also keep the complete reported table, every entry in its
redacted form, inside one frame on every transport the device exposes,
so that a CMD_PROP_GET always answers in one piece. At under forty
octets per reported entry that constrains nothing a device would want.
Removing the selected network, whether by CMD_PROP_REMOVE or a
whole-table CMD_PROP_SET that omits it, clears the selection: the
device drops the association, publishes PROP_WIFI_NETWORK as empty,
and publishes the link going down. Replacing the selected network’s
entry drops any association it holds and starts a fresh join with the
new entry at once, because a host that has just corrected a passphrase
should not wait out a backoff to learn whether it worked.
The table is part of the saved snapshot, credentials included, which is
what lets the device rejoin unattended. CMD_CLEAR erases the
persisted copy with everything else and, as with everything else,
leaves the live table alone: the device stays on its network until the
CMD_RST that completes a factory reset reverts the table to its
now-empty post-reset value. A device advertising CAP_SAVE stores it
as it stores key material.
PROP 4882: PROP_WIFI_NETWORK
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_WIFI - Value Type: 1 to 32 octets (an SSID), or empty
- Post-Reset Value: Empty, or restored from saved state
The network the device is on, or is to be on: the SSID of one entry in
PROP_WIFI_NETWORKS. This is “connect” and
“disconnect” both.
Writing an SSID selects that network. If the station is enabled, the device drops any current association and starts joining the new one; if it is not, the selection waits for it, so a whole configuration can be staged and the station enabled last. Writing the SSID that is already selected changes nothing and disturbs nothing, like any other property written with its own value.
An SSID not in the table is refused with STATUS_ITEM_NOT_FOUND: the
credential lives in the table, and the selection only names it. A host
acts on that differently from STATUS_INVALID_ARGUMENT, one being
“insert it first” and the other a bug.
Writing the empty value deselects. The device leaves the network and associates with nothing until something is selected again. This is a stable state, not a moment: the station stays up, scans on request, and joins nothing, which is what a phone with Wi-Fi on and no network in range is doing. A device MUST NOT select a network on its own, because a disconnect the device undoes by itself is not one.
The device MUST publish the property when it changes it: a removal from the table that empties the selection, or a picker on the device’s own screen.
The selection is the network the device will spend its unattended life trying to reach, so it is saved with the table. A device restored from a snapshot comes up joining what it was joining.
PROP 4883: PROP_WIFI_SCANNING
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_WIFI_SCAN - Value Type: BOOL
- Post-Reset Value: whether a scan is running
Whether a scan is in progress. Writing 1 starts one; the device
answers with 1, reports each access point as it is heard through
PROP_WIFI_SCAN_RESULTS, and publishes 0
when the scan completes. Writing 0 abandons a scan in progress and
leaves in the results whatever had been found by then.
The frames of a scan MUST go out in this order, so that a host can never misattribute one:
CMD_PROP_ISforPROP_WIFI_SCAN_RESULTScarrying the empty value: the previous results are gone.CMD_PROP_ISfor this property carrying1, as the reply to the host’s write or unsolicited when the device started the scan itself.- One
CMD_PROP_INSERTEDper access point heard, in the order heard. CMD_PROP_ISfor this property carrying0, after the last of them.
The clear goes first so that every insert lands in a table the host
knows to be empty, and the completion goes last so that a host seeing
0 holds the whole list without reading it.
This is a pairing window’s shape exactly: a state the host can enter, that ends by itself, and that a device with a screen can enter without the host. A device MUST bound a scan’s duration; a few seconds is what the hardware takes.
On a device with a station, a write of 1 answers
STATUS_INVALID_STATE while the station is disabled, and MAY
answer STATUS_BUSY while a join is in the middle of its handshake,
which resolves by itself. On a device without one there is nothing to
enable: the receiver is powered for the scan’s duration and put back to
sleep after, which is the right power shape for a tracker that scans a
few times an hour. Writing 1 during a scan succeeds and answers 1;
there is nothing to restart. A write of 0 always succeeds.
A device MAY scan while associated, at the cost of the association’s traffic while it is off-channel. Whether the scan is active or passive, and on which channels, is the device’s business, except that a device with a station MUST probe by name for a hidden network in its table so that it can appear.
A scan the device starts from its own menu is reported the same way, inserts and all. An attached host pays twenty-odd small frames once per scan, which is nothing against the scan itself, and a picker that appears as networks are heard is the difference between a list that fills in and a spinner.
PROP 4884: PROP_WIFI_SCAN_RESULTS
- Type: Multiple-Value, Read-Only
- Has Item Length Prefix: Yes
- Asynchronous Updates: Yes (
Is,Inserted) - Required:
CAP_WIFI_SCAN - Post-Reset Value: what the receiver has found; empty after a power-on
What the scan in progress has found so far, or what the last one found. Each item:
+-------+-----------+------+--------+-----------+
| MODES | FREQUENCY | RSSI | BSSID | SSID ... |
+-------+-----------+------+--------+-----------+
2 B 2 B, MHz 1 B 6 B 0-32 B
Figure: Scan result item format
MODES is a 16-bit little-endian set of the security modes the access point offers, bit n standing for mode n of the enumeration above. No bits at all means the device did not determine them: a receiver that reads beacon headers for their addresses has no reason to parse the security elements, and no real access point offers nothing, so the empty set is free to mean that.
A WPA2/WPA3 transition network sets both bits. An OWE transition
network is two BSSs, a visible open one and a hidden OWE companion that
the open one’s transition element names by BSSID and SSID; the device
reads the element and reports the visible BSS with both the open
and the OWE bit set, so the host sees one network offering two modes
and picks between them like any other. The companion is reported as
itself, under the SSID the element gave it, and nothing depends on it.
A set rather than a single strongest value, because which of the
offered modes the host should write depends on what the device can do,
and the scan result is not the place to guess: the host writes the
strongest bit it likes and steps down on STATUS_UNIMPLEMENTED.
FREQUENCY is the center frequency of the access point’s primary 20 MHz channel, in megahertz, little-endian. A frequency rather than a channel number because a channel number is ambiguous across bands and a frequency is not, and the number follows from the frequency in one line wherever a display wants it. The width of the operating channel is not reported: the device negotiates it at association, and nothing about it is needed to join.
RSSI is a signed dBm. SSID is the remainder of the item, and empty means no name was reported, because the network hides it or because the scanner does not read names; the BSSID is what distinguishes one nameless entry from the next. A device with a station always reads names, so on such a device empty means hidden. A picker treats the two alike in any case, since neither can be chosen by name.
One item per access point, keyed by BSSID, and never the device’s own: a device that is also an access point MUST NOT report itself. The device reports what it heard and nothing it inferred. Which of several access points make up one network is a question the host answers by grouping on SSID, and a host that wants the list a phone shows coalesces, keeps the strongest per name, and sorts. A host that wants to see every radio in the building has that too. Hidden networks need no special case, since an access point with no name still has an address.
The device reports each access point with CMD_PROP_INSERTED as it is
heard, and hearing one again is another CMD_PROP_INSERTED under the
same BSSID, which replaces the host’s entry. Replacement by key is
what makes the inserts idempotent, and idempotence is what makes it
safe for a host to read the table mid-scan and follow the inserts from
there: an item that arrives in both the reply and a notification is the
same item twice.
The device retains a bounded table and reports an unbounded scan. Every access point heard is inserted; the device keeps the strongest of them up to its bound, evicting the weakest as stronger ones arrive, and never reports an eviction. The bound MUST be chosen so that the whole retained value fits in one frame on every transport the device exposes; on BLE that is the 512-octet reassembled frame, into which twenty or so typical items fit. The inserts are one item each and never approach it.
This is the one place the property model bends on purpose. A host that
followed the inserts holds a superset of what a CMD_PROP_GET returns:
everything the scan heard, against the strongest twenty the device
kept. It is harmless because nothing in the host’s copy is invented,
every item in it was heard, and the two are reconciled by the clear at
the next scan. The alternative, a CMD_PROP_REMOVED per eviction,
would spend frames telling the host to stop showing an access point it
can see, in order to keep two views identical that nobody needs to
compare. Where removals do describe something a host wants, as they do
for access point clients, they are sent.
CMD_PROP_GET returns the retained table strongest first. Inserts
arrive in the order the access points were heard, which is the order
the host receives them in and has no other meaning.
Delivering results as they are found is a promise about delivery, not about pace. Most stacks hand back a scan only when it finishes; a device gets progressive results by scanning a channel at a time, which the usual scan interfaces allow and which costs a little total duration for first results in a fraction of a second. A device that cannot do that emits every insert at the end and conforms.
The value is cleared, and the empty value published with
CMD_PROP_IS, when a scan starts and when the station is disabled. A
list of what was in the air somewhere the device may no longer be is
worse than an empty one.
PROP 4885: PROP_WIFI_LINK
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_WIFI - Value Type: structure below
- Post-Reset Value: what the station is doing
+-------+--------+--------+-----------+
| STATE | REASON | BSSID | FREQUENCY |
+-------+--------+--------+-----------+
1 B 1 B 6 B 2 B, MHz
(present only when STATE is WIFI_LINK_UP)
Figure: Link state format
STATE:
| Value | Name | Meaning |
|---|---|---|
| 0 | WIFI_LINK_DOWN | Not trying: the station is off, or nothing is selected |
| 1 | WIFI_LINK_CONNECTING | A network is selected and the device is not on it yet, or not any more |
| 2 | WIFI_LINK_UP | Associated |
REASON says why the device is in WIFI_LINK_CONNECTING rather than
WIFI_LINK_UP, and is 0 in the other two states:
| Value | Name | Meaning |
|---|---|---|
| 0 | WIFI_REASON_NONE | No attempt has failed yet |
| 1 | WIFI_REASON_NOT_FOUND | The network was not heard and did not answer a probe |
| 2 | WIFI_REASON_AUTH | The network rejected the credential |
| 3 | WIFI_REASON_REJECTED | The network refused the association for another reason |
| 4 | WIFI_REASON_LOST | The association was up and dropped |
| 5 | WIFI_REASON_OTHER | Something the device has no name for |
WIFI_LINK_UP is an 802.11 statement: the station is authenticated and
associated. Whether the device has an address on the link it is now on
is a different layer’s fact and belongs to the family state
properties, which report it.
WIFI_LINK_CONNECTING is the whole of trying, including the waits
between attempts. While the station is enabled and a network is
selected, the device MUST retry indefinitely with backoff and
MUST NOT give up: a wrong passphrase is a device that retries a few
times an hour until someone fixes it, which costs nothing and is what
unattended infrastructure should do. WIFI_LINK_DOWN is reserved for
the two states in which the device is not trying at all, so that a host
reading DOWN knows the fix is configuration and a host reading
CONNECTING knows the fix is in the reason.
The device MUST publish the property on every change of state and
on every change of reason, and on nothing else. Retrying and failing
the same way again is not a transition and is not published, so a
device with a wrong passphrase reports WIFI_REASON_AUTH once, not
every attempt.
When WIFI_LINK_UP, the value carries the association: which access
point, and the center frequency of its primary channel in megahertz, as
in a scan result. A roam to another access point of the same network is
a change of value and is published.
Live state: NOT part of the saved snapshot, and CMD_RST reaches
it only as a consequence of reverting the configuration it follows.
PROP 4886: PROP_WIFI_RSSI
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required:
CAP_WIFI - Value Type: INT8 in dBm, or empty
- Post-Reset Value: the current measurement; empty when the link is not up
The received signal strength of the current association, measured when the property is read. Empty when the link is not up.
Kept out of PROP_WIFI_LINK for the reason PROP_GNSS_LOCATION is
kept quiet: a measurement that changes on every beacon has no business
in a property that is published on every change.
PROP_PHY_RSSI is the same split on the
LoRa side.
PROP 4887: PROP_WIFI_MAC
- Type: Single-Value, Constant
- Asynchronous Updates: No
- Required:
CAP_WIFI - Value Type: 6 octets
The station’s MAC address, as it appears to the access point. Constant because a router’s allow list is keyed on it, and a device that randomized it would be reporting an address nobody can use.
PROP 4912: PROP_WIFI_AP_ENABLED
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_WIFI_AP - Value Type: BOOL
- Post-Reset Value: 0 (false), or restored from saved state
Whether the access point is up. Set, the device beacons the configured network, accepts stations onto it, and serves them addresses; cleared, it MUST disassociate every client, stop beaconing, and release whatever the access point held of the radio. The configuration is untouched in both directions.
Off by default, and a write of 1 while
PROP_WIFI_AP_CONFIG is empty answers
STATUS_INVALID_STATE. Together those two rules mean there is no
factory network: no default name a stranger can look up, no default
passphrase, and no open network a device falls back to because nobody
configured one. A device that has never been given a network to offer
offers nothing.
A write of 1 answers STATUS_INVALID_STATE on a platform that cannot
run the access point alongside a radio it currently has on, the station
included, and the device MUST NOT take the other down to honor the
write. A platform that runs both, which is most of them, answers the
write like any other.
Asynchronous for the reason every switch here is: a device with a screen MAY offer it, and the device takes the access point down by itself when its configuration is cleared.
PROP 4913: PROP_WIFI_AP_CONFIG
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_WIFI_AP - Value Type: structure below, or empty
- Reported Form: the structure with its credential omitted
- Post-Reset Value: Empty, or restored from saved state
+-------+----------+-------------+-----------+---------+--------+----------+--------+----------------+
| FLAGS | SECURITY | MAX_CLIENTS | FREQUENCY | ADDRESS | PREFIX | SSID_LEN | SSID | CREDENTIAL ... |
+-------+----------+-------------+-----------+---------+--------+----------+--------+----------------+
1 B 1 B 1 B 2 B, MHz 4 B 1 B 1 B 1-32 B security-defined
Figure: Access point configuration format
FLAGS, SECURITY, SSID_LEN, SSID, and CREDENTIAL are
as in a PROP_WIFI_NETWORKS item, with the same
encodings, the same well-formedness rules, and STATUS_INVALID_ARGUMENT
for the same faults. The modes an access point may offer are
WIFI_SEC_OPEN, WIFI_SEC_OWE, WIFI_SEC_WPA2, and WIFI_SEC_WPA3;
any other is refused with STATUS_INVALID_ARGUMENT, because no
credential form for offering it is defined here, and a device without
SAE refuses WIFI_SEC_WPA3 with STATUS_UNIMPLEMENTED as it does in
the table. A hidden network beacons without its name and answers probes
for it; the flag is a courtesy to neighbors’ pickers and not a secret,
since the name is in every association.
MAX_CLIENTS is how many stations the device admits at once, or 0
for the device’s own limit. A device bounds it to what its stack can
hold and to what keeps
PROP_WIFI_AP_CLIENTS in one frame, and
clamps a larger write to that bound rather than refusing it, reporting
the clamped value: the host asked for “many”, and the device’s most is
the honest answer.
FREQUENCY is the center frequency of the primary channel the access
point is to use, in megahertz, or 0 for the device’s choice. It is a
preference, not a promise. A frequency the device may not use under its
regulatory configuration is refused with STATUS_INVALID_ARGUMENT, and
one it may use is what the access point beacons on while the station
is not associated. On hardware where the access point and the station
share one radio, and therefore one channel, the access point sits on
the station’s channel while the station is up, whatever this field
says, and moves when the station roams.
PROP_WIFI_AP_STATE reports where the access
point actually is.
ADDRESS and PREFIX are the device’s own IPv4 address on the
network it offers and the prefix of that network, or all-zero and 0
for the device’s default, which SHOULD be 192.168.4.1/24 since
that is what a phone joining an embedded device has come to expect. The
device serves DHCP on this subnet, handing out addresses within the
prefix other than its own, and answers as the gateway and resolver a
lease names, whether or not it can forward anything. The prefix is 8 to
30. An address that is not a usable unicast address in the sense
PROP_IPV4_STATE defines, or one that
lies within the subnet the station holds, is refused with
STATUS_INVALID_ARGUMENT. IPv6 on the offered network is link-local
only, which needs no configuration and is not reported.
This is where the access point’s addressing lives, and not in the IP Connectivity properties, which describe the interface the device joins a network with and only that. Those properties configure how a device gets onto someone else’s network; the access point’s subnet is not a configuration the network hands the device but one the device imposes, it never changes without a write here, and a fixed address in the same structure as the network it belongs to is the shape every embedded access point already has.
The credential is write-only under Provisioning Security, and the reported form stops at the SSID. This is a passphrase the operator hands to other people, which makes a case for reading it back, and the case loses. The reader is a party that could write any passphrase it liked, so reading one back leaks nothing but the ability to rotate it without disturbing clients; a device with a screen MAY show it or a QR code for it; and one rule for every credential on the device is worth more than that convenience. It follows that every write of a secured configuration carries its credential, since there is nothing to leave in place.
A write takes effect at once. While the access point is up, the device
disassociates every client, brings the network up again under the new
configuration, and publishes the client list going empty; a client that
knows the new credential rejoins on its own. Writing the empty
value clears the configuration and is refused with
STATUS_INVALID_STATE while the access point is enabled, so that no
write here ever takes the network down as a side effect. The host
disables it first.
Saved with the switch, credential included, so that a device commissioned to offer a network offers it at every boot.
PROP 4914: PROP_WIFI_AP_STATE
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_WIFI_AP - Value Type: structure below
- Post-Reset Value: what the access point is doing
+-------+-----------+
| STATE | FREQUENCY |
+-------+-----------+
1 B 2 B, MHz
(present only when STATE is WIFI_AP_UP)
Figure: Access point state format
STATE:
| Value | Name | Meaning |
|---|---|---|
| 0 | WIFI_AP_DOWN | Not beaconing: disabled, unconfigured, or refused the radio |
| 1 | WIFI_AP_UP | Beaconing and accepting stations |
FREQUENCY is the center frequency of the primary channel the access point is beaconing on, which is the configured one while the station is not associated and the station’s while it is. This is the field the property exists for: the configuration says what was asked, and only the state says where the network actually is, which a host that wants to tell a person which channel to expect has no other way to learn.
The device MUST publish it on every change of state and every change of frequency, so that a station roam which drags the access point to a new channel is reported, and on nothing else. Two states and no reason, because every way of being down is readable from the two configuration properties and the station’s link, and a reason octet would only quote them.
PROP 4915: PROP_WIFI_AP_CLIENTS
- Type: Multiple-Value, Read-Only
- Has Item Length Prefix: Yes
- Asynchronous Updates: Yes (
Is,Inserted,Removed) - Required:
CAP_WIFI_AP - Remove Selector: the MAC
- Post-Reset Value: the stations currently associated; empty when the access point is down
The stations on the device’s network. Each item:
+-------+------+---------+
| MAC | RSSI | ADDRESS |
+-------+------+---------+
6 B 1 B 4 B
Figure: Access point client item format
MAC is the client’s, and the key. RSSI is a signed dBm as the device last heard the client. ADDRESS is the IPv4 address the device’s DHCP server leased it, or all-zero until it has one, which is what lets a host reach a client it can see.
The device publishes CMD_PROP_INSERTED when a station associates and
again, under the same MAC, when its address is leased or changes, which
replaces the host’s entry as a scan result does; CMD_PROP_REMOVED
with the MAC when a station leaves or is dropped; and CMD_PROP_IS
with the empty value when the access point goes down. RSSI is reported
as it stood at the last of those and is not published on its own, under
the rule that keeps a beacon-rate measurement out of every published
property here.
This is the one multi-value property in this chapter that emits
CMD_PROP_REMOVED, and the difference from
PROP_WIFI_SCAN_RESULTS is not a matter of
taste. The scan table withholds removals because they would describe an
eviction from a bounded view of an unbounded list, telling the host to
stop showing something real. A client leaving is a fact about the world
that the host wants, the list is bounded by MAX_CLIENTS to something
small, and the device’s view and the host’s are meant to be the same
view, so the ordinary property model applies without amendment. The
bound MUST keep the whole list in one frame, which at eleven octets
an item constrains nothing.
Sharing the Radio
One radio, one channel. Where the access point and the station are
two interfaces on one transceiver, the transceiver is on one channel.
While the station is associated the access point is on the station’s
channel; when the station roams, the access point moves with it and its
clients drop and rejoin, which is a few seconds of disruption the
device did not choose and cannot avoid, and which
PROP_WIFI_AP_STATE reports as a frequency
change. A host that needs the access point to stay put keeps the
station off, or on a network with one access point.
A scan is a gap. Scanning takes the transceiver off channel. Every
client of the access point loses its beacons for the duration, which
most stations tolerate and some do not, and an association loses its
traffic. On a device whose Wi-Fi receiver is its LoRa transceiver, a
scan is also a gap in mesh reception lasting as long as the scan does,
during which nothing on the air is heard and nothing is forwarded. The
host asked for that, and it is not free: a repeater’s operator scanning
on a schedule is spending the mesh’s reliability. A device MAY
answer STATUS_BUSY to a scan request rather than abandon a
transmission in progress or drop the clients it is serving.
Enable in either order. The two switches are independent, and a
platform that cannot run both refuses the second 1 with
STATUS_INVALID_STATE and leaves the first alone. Nothing about the
station’s table, selection, or link changes when the access point comes
up or goes down, and nothing about the access point’s configuration
changes with the station’s.
Forwarding is not specified. Whether a client of the access point can reach the network the station is on, and how, is a policy about what the device does with its two interfaces. A device that forwards nothing is a conforming access point, and a useful one to a phone that only wants to reach the device.
Regulatory. The channel set follows the platform’s regulatory
configuration, with one asymmetry: a station on the wrong channel is a
receiver that hears nothing, and an access point on the wrong channel
is a transmitter beaconing where it may not. A device advertising
CAP_WIFI_AP therefore MUST refuse a FREQUENCY outside its
regulatory configuration, and MUST choose within it when the field
is 0.
Host Procedures
The flows a host runs, in terms of the properties above. None of them needs anything the property grammar does not already provide.
Turn on. Write PROP_WIFI_ENABLED to 1. If a network is
selected, PROP_WIFI_LINK publishes CONNECTING and then UP or a
reason.
Scan. Write PROP_WIFI_SCANNING to 1; clear the list on the
CMD_PROP_IS that follows; add or replace an entry per
CMD_PROP_INSERTED, grouping by SSID for display; stop the spinner on
the unsolicited 0. A host on a binding that carries no notifications
polls PROP_WIFI_SCANNING and reads PROP_WIFI_SCAN_RESULTS once it
reads 0. A host presenting a picker marks each network already in
PROP_WIFI_NETWORKS by matching SSIDs, since the reported table form
carries them.
Join a new network. Insert an entry in PROP_WIFI_NETWORKS with
the SSID from the scan result, the strongest mode in its offered set,
and the passphrase from the operator; on STATUS_UNIMPLEMENTED, insert
again with the next mode down. Then write the SSID to
PROP_WIFI_NETWORK. Two writes rather than one because the credential
has one home and the selection only names it. There is no safe
one-frame shortcut: a whole-table CMD_PROP_SET replaces every entry,
and since credentials cannot be read back, a host can only write a
whole table it holds every credential for. That is the first host
commissioning a fresh device, and nobody after it.
Fix a wrong passphrase. PROP_WIFI_LINK reads CONNECTING with
WIFI_REASON_AUTH. Insert the entry again with the corrected
credential; the device tries it immediately and the link reports the
outcome.
Switch networks. Write the other SSID to PROP_WIFI_NETWORK.
Reconnect. There is no such flow. The device is already retrying, and a host that wants a fresh start with the same credentials has the honest two-step: deselect, then select, each of which is a real state.
Disconnect. Write PROP_WIFI_NETWORK empty. The device leaves and
stays off the network with the station still up.
Forget. Remove the entry from PROP_WIFI_NETWORKS. If it was
selected the selection empties and the link drops, both published.
Turn off. Write PROP_WIFI_ENABLED to 0. Nothing is forgotten.
Locate. On a device with CAP_WIFI_SCAN and no station, run the
scan flow and hand the BSSIDs and signal levels to whatever resolves
them into a position. The item format is what a geolocation resolver
consumes, and a phone passes the list straight through. What the device
does with its own scan, resolving on board or sending access points
over the air, is application and is not here.
Offer a network. Write PROP_WIFI_AP_CONFIG with the SSID, mode,
and credential, then PROP_WIFI_AP_ENABLED to 1. Watch
PROP_WIFI_AP_STATE for the channel it landed on and
PROP_WIFI_AP_CLIENTS for who arrives.
Commission for unattended use. Do any of the above, then
CMD_SAVE. The station comes up and rejoins, and the access point
comes up, at every boot after that.
Synchronize on attach. Read PROP_WIFI_ENABLED,
PROP_WIFI_NETWORK, PROP_WIFI_LINK, and, where the device has one,
PROP_WIFI_AP_ENABLED and PROP_WIFI_AP_STATE, in one
CMD_PROP_MULTI_GET where available. The tables are read only when the
host needs to show them.
What Survives What
| Property | Saved | CMD_RST | Detach | Station off |
|---|---|---|---|---|
PROP_WIFI_ENABLED | yes | reverts | kept | — |
PROP_WIFI_NETWORKS | yes | reverts | kept | kept |
PROP_WIFI_NETWORK | yes | reverts | kept | kept |
PROP_WIFI_SCANNING | no | follows | kept | abandoned |
PROP_WIFI_SCAN_RESULTS | no | follows | kept | cleared |
PROP_WIFI_LINK | no | follows | kept | DOWN |
PROP_WIFI_RSSI | no | follows | kept | empty |
PROP_WIFI_MAC | — | — | — | — |
PROP_WIFI_AP_ENABLED | yes | reverts | kept | — |
PROP_WIFI_AP_CONFIG | yes | reverts | kept | — |
PROP_WIFI_AP_STATE | no | follows | kept | — |
PROP_WIFI_AP_CLIENTS | no | follows | kept | — |
“Reverts” means to the post-reset value, which on a device with a
snapshot is the saved one. “Follows” means the live state ends up
wherever the reverted configuration puts it, and nowhere else. A
CMD_RST on a device whose live configuration already matches its
snapshot leaves a running scan running, an association up, and an
access point beaconing; one that reverts the selection or its
credential drops the association and joins the restored entry; and one
on a device with no snapshot takes the station and the access point
down and everything live with them.
CMD_RESTORE is the same column: in either of its forms it reverts the
configuration properties to the snapshot and the live state follows.
Detach touches nothing, because the station and the access point are
device-domain and run unattended, and a scan that was in progress when
the host left completes and leaves its results for the next one.
Over the Node Management Binding
Every property here is device-domain, so a listed administrator over the mesh may read all of them and write the configuration ones, under the same rule that lets channel keys be provisioned remotely: the binding already delivers each request authenticated and encrypted, so an administrator may provision a credential.
Reading is the redacted form and nothing else, here as on the local link. The network table and the access point’s configuration read back without their credentials, and no binding exists over which a credential can be read.
That binding carries no unsolicited notifications, so a scan is the
polled flow above, and an administrator that wants the client list
reads it while PROP_WIFI_AP_STATE says there is one to read. Scan
results are the one large read, and the binding’s cursors carry them.
An administrator who disables the station a bridge tunnel rides on has
done the same thing as one who writes PROP_MAC_BACKHAUL, and warrants
the same warning in the same place.
On BLE
The frame-size concerns are a CMD_PROP_GET of the scan results, of
the network table, and of the client list, and all three are bounded
above so that they fit. The inserts that deliver scanned access points
live are one item each. The largest credential write, a 32-octet SSID
with a 128-octet SAE password, is under two hundred octets with
framing. Everything else is a few.
Security Considerations
- A passphrase crosses the link once, inbound, over a transport that meets the provisioning requirement, and is never reported. A later host on the same device cannot extract an earlier host’s Wi-Fi credentials any more than its channel keys.
- The security mode is exact. A device joins with the mode the entry names and no other, whatever the network in front of it advertises, so a downgrade has to be written by the host rather than offered by the air.
- Scan results are what the device heard, and an SSID is what its sender chose to call itself. A host displays them as untrusted strings.
- A scan result list is a location fingerprint of wherever the device is standing, precise to a building, which is exactly why a tracker wants one. It is readable only by an admitted party, like everything else here, and a device that resolves positions on board treats what it learned the way it treats a fix.
- An access point is the one thing here that announces the device to everyone in range. Its SSID and its MAC are in every beacon, a hidden network is hidden from pickers and not from anyone listening, and the name it beacons MUST NOT carry the device’s mesh identity or any part of its address. There is no factory network: the access point offers nothing until a host configures it, and its passphrase is write-only like every other credential.
PROP_WIFI_MACand the SSIDs in the table are identifying. They are no more so thanPROP_DEV_NAME, and they are readable only by a party that has already been admitted.- A device on Wi-Fi is a device on a LAN, and a device with an access point is a device hosting one. This chapter gives it an address and nothing that listens on one. Any service the device later offers over the interface, a ULCP binding over TCP above all, carries the full authority of an attached host and needs an admission ceremony of its own before it exists; the serial transports’ physical-possession argument does not extend to a network port, and it extends least of all to a network the device invited the client onto. Whether knowing the access point’s passphrase is itself such a ceremony is a decision for the binding that would rely on it.
Not Specified
Deliberately absent, with the reason:
- Anything on the access point’s network. Forwarding between the offered network and the station’s, a captive portal, a provisioning flow, or a binding a client could attach through: each is a thing the device does with the network rather than the network itself, and the last needs the admission ceremony above before it can exist.
- Enterprise authentication. Certificates and identities are a provisioning surface an order of magnitude larger than a passphrase. The three enterprise modes have numbers so that scan results can name them and a host can explain why a network is unavailable; the credential forms come with the capability that opens them.
- Negotiation details. Management frame protection, SAE hash-to-element, transition-disable: the device does what the mode requires and the host never sees them. WPS and Easy Connect are provisioning methods rather than modes.
- Auto-join across the table. There is none. The device joins the network it was told to and no other, because a device that picks networks by itself is a device whose behavior depends on what is in the air around it, which is the wrong property for a repeater on a wall.
- Regulatory country. The channel set follows the platform’s regulatory configuration, which may later be tied to the device’s region; nothing here decides how that configuration is set. What is decided is that an access point stays inside it.
- Power-save mode, PHY rate, band preference. The device’s business. A property that exposes them is easy to add and hard to remove.
- A reconnect request. The one genuinely command-shaped act in the vicinity is deliberately not disguised as a property: a property write with a side effect when written with the value it already holds is a command in a costume, and it breaks the moment a host replays its configuration. It is also unnecessary, since the device retries on its own and replacing the selected entry restarts the join at once.
ULCP: IP Connectivity
The layer above a link: whether the device can reach anything, what it is reachable at, and how to configure the cases the network does not configure for it.
Address configuration is not a property of Wi-Fi. A wired link, should
one ever appear, needs exactly these properties, and a device’s IP
stack is one thing whichever link carries it. Everything here is
therefore named IP rather than after a link, and nothing in it knows
what the link is.
Nothing about what the device reaches belongs here. A bridge tunnel or a time source reports its own state under its own capability.
These properties describe one interface: the one the device uses to join a network, which is the station’s on a device whose only link is Wi-Fi. A device’s own access point is a second interface and is deliberately not described here, because its addressing is a subnet the device imposes rather than one a network hands it, and it lives in that access point’s own configuration. A device with a second link of the joining kind is a future revision, and these properties are shaped so that an interface selector could be added without renaming them.
The two configuration properties and the configured resolver list are
device-domain state and part of a saved
snapshot. The five that report what
the stack currently holds are live: never saved, and reached by
CMD_RST only through the configuration they follow.
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 55 | CAP_IPV4 | — | An IPv4 stack on the device’s link: PROP_IPV4_STATE, PROP_IPV4_CONFIG, PROP_IPV4_ADDRESS, and the shared PROP_IP_DNS and PROP_IP_RESOLVERS |
| 56 | CAP_IPV6 | — | An IPv6 stack on the device’s link: PROP_IPV6_STATE, PROP_IPV6_CONFIG, PROP_IPV6_ADDRESSES, and the same two shared properties |
One capability per family, because the families are peers. The BLE binding argues for a single capability on the grounds that a refusal is a complete answer about an extra, and neither family is an extra to the other: a device that speaks only IPv4 is ordinary today, and a device that speaks only IPv6 is an ordinary device on an IPv6-only network tomorrow. Making either the floor would encode which one is normal, which is a fact about the year rather than about the protocol.
A device with both advertises both, and a host that sees either knows
the two shared properties are there. The seam stops at the family: DHCP
versus static, advertisements versus DHCPv6, are methods within a
family, and a device that lacks one refuses the write with
STATUS_UNIMPLEMENTED as that argument intends.
Neither capability formally requires a link capability, on purpose. A
capability’s requirements are concrete codes, and naming CAP_WIFI
here would make a wired device either lie about having Wi-Fi or invent
a second pair of IP capabilities. The precondition is stated instead: a
device advertising either has a stack on one link, and describes that
link through whatever link capability it also advertises. A device that
advertises an IP capability and no link capability has a link it offers
no control over, which is a legal shape for a device with a fixed wired
port; its family states simply never report IP_NO_LINK for a reason
the host can act on.
Properties
Allocated in the block after the Wi-Fi station’s.
| Id | Mnemonic | Commands | Description |
|---|---|---|---|
| 4896 | PROP_IPV4_STATE | Get, Is | IPv4 readiness |
| 4897 | PROP_IPV4_CONFIG | Get, Set | How IPv4 is configured |
| 4898 | PROP_IPV4_ADDRESS | Get, Is | The IPv4 address, prefix, and gateway in effect |
| 4899 | PROP_IPV6_STATE | Get, Is | IPv6 readiness |
| 4900 | PROP_IPV6_CONFIG | Get, Set | How IPv6 is configured |
| 4901 | PROP_IPV6_ADDRESSES | Get, Is | The IPv6 addresses and default routers in effect |
| 4902 | PROP_IP_DNS | Get, Set, Insert, Remove | Configured resolvers, or empty to use what the network provides |
| 4903 | PROP_IP_RESOLVERS | Get, Is | The resolvers in use |
4904 through 4911 are reserved for this subsystem.
PROP 4896: PROP_IPV4_STATE
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_IPV4 - Value Type: UINT8
- Post-Reset Value: what the family is doing
One octet, from the enumeration both family states share:
| Value | Name | Meaning |
|---|---|---|
| 0 | IP_DISABLED | The family is configured off |
| 1 | IP_NO_LINK | The link is down, so there is nothing to address |
| 2 | IP_WAITING | The link is up and the family has no usable address yet |
| 3 | IP_READY | The family holds a usable address |
| 4 | IP_CONFLICT | The configured static address is held by something else |
A usable address is a unicast host address that is not link-local:
for IPv4 anything outside 169.254/16 that is neither multicast,
broadcast, loopback, nor unspecified, and for IPv6 anything outside
fe80::/10 under the same exclusions. A self-assigned 169.254
address reaches only the link, exactly as fe80:: does, and a device
that fell back to one is a device whose DHCP failed, which
IP_WAITING says and IP_READY would hide. A device with only an IPv6
link-local address is IP_WAITING, because it is waiting for exactly
the advertisement that would give it a usable one. This is the boundary
readiness is defined on, and the same boundary the static configuration
is validated against.
This is the property a host watches for its family. It changes when a lease is obtained or lost, when a router starts or stops advertising, and when the link comes and goes, which is a few times in a session rather than a few times a minute. The device MUST publish it on any change and on nothing else. The address and resolver properties publish their own changes, since those can move while the state stands still.
IP_WAITING is this layer’s
WIFI_LINK_CONNECTING: the device is
doing what its configuration says and the network has not answered. It
carries no reason, because the reasons are the network’s, a DHCP server
that does not answer or a router that does not advertise, and the fix
is on the network.
IP_CONFLICT is the exception that earns its own value. A static
address that duplicate-address detection or an ARP probe finds already
in use is a fault in the configuration this protocol wrote, the fix is
a different address, and a host that could not tell it from an ordinary
wait would tell the operator to check the router. The device keeps
probing while in IP_CONFLICT and moves to IP_READY if the other
holder goes away. Under IP_METHOD_AUTO a conflict is the stack’s to
resolve, by declining the lease and asking again, and the family stays
IP_WAITING.
IP_NO_LINK is separate so that a host can tell “the station is not
associated” from “the station is associated and nobody is handing out
addresses” without reading PROP_WIFI_LINK as well.
IP_READY means an address, not a route. Whether the family also has a
default route is in the address property, the gateway field for IPv4
and the router items for IPv6, and a device on an isolated network that
hands out addresses and no gateway is ready by this definition, which
is the honest one.
PROP 4897: PROP_IPV4_CONFIG
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_IPV4 - Value Type: structure below
- Post-Reset Value:
IP_METHOD_AUTO, or restored from saved state
+--------+---------+--------+---------+
| METHOD | ADDRESS | PREFIX | GATEWAY |
+--------+---------+--------+---------+
1 B 4 B 1 B 4 B
(present only when METHOD is IP_METHOD_STATIC)
Figure: IPv4 configuration format
METHOD:
| Value | Name | Meaning |
|---|---|---|
| 0 | IP_METHOD_DISABLED | The family is not used on the link |
| 1 | IP_METHOD_AUTO | DHCP |
| 2 | IP_METHOD_STATIC | The address, prefix, and gateway that follow |
The default is IP_METHOD_AUTO, so that a device with nothing
configured is on the network the moment it is associated; this
subsystem exists for the cases where that is not enough.
A PREFIX above 32, a static form of the wrong length, or a static
address that is not usable in the sense
PROP_IPV4_STATE defines, 169.254/16 included,
is refused with STATUS_INVALID_ARGUMENT. A GATEWAY is either
all-zero or a unicast address that is not multicast, broadcast,
loopback, or link-local, and anything else is refused the same way; the
IPv6 form relaxes the last exclusion, since a router names itself by
its link-local address and a static IPv6 gateway is usually exactly
that. All-zero means no default route, which is what a device on a
network with no way out should be told.
A write takes effect at once. On a device that is IP_READY under the
old configuration, the old address is released and the new one applied,
and PROP_IPV4_STATE reports the transitions like any other. A write
while the link is down is accepted and waits for it, so that a static
configuration can be staged before the link is enabled.
PROP 4898: PROP_IPV4_ADDRESS
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_IPV4 - Value Type: 4-octet address, 1-octet prefix, 4-octet gateway; or empty
- Post-Reset Value: the address in effect; empty when the family is not
IP_READY
The IPv4 address the interface holds, its prefix length, and the
default gateway, all-zero when there is none. Whatever the method:
under IP_METHOD_AUTO this is what the lease said, and under
IP_METHOD_STATIC it is what was written, once the device holds it.
The device MUST publish it whenever the reported value changes.
Mostly that is when PROP_IPV4_STATE moves, but not only then: a lease
renewal can keep the address and change the gateway, and a host that
read the value once and watched only the state would carry the old
gateway forever. A renewal that changes nothing publishes nothing.
PROP 4899: PROP_IPV6_STATE
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_IPV6 - Value Type: UINT8
- Post-Reset Value: what the family is doing
IPv6 readiness, from the enumeration
PROP_IPV4_STATE defines, with the same
publication rule and the same meaning for every value. The one
family-specific note is the link-local boundary: a device holding only
an fe80:: address is IP_WAITING, because it is waiting for exactly
the advertisement that would give it a usable one.
Two properties rather than two octets in one, because the capabilities
are two. A device without CAP_IPV6 would otherwise carry an octet
describing a family it does not have, and a property granted by “either
capability” is a property with two homes. One octet per family costs a
second notification when a link drop takes both families down, which is
one small frame at a moment the host is already being told things.
PROP 4900: PROP_IPV6_CONFIG
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_IPV6 - Value Type: as
PROP_IPV4_CONFIGwith 16-octet addresses - Post-Reset Value:
IP_METHOD_AUTO, or restored from saved state
The same structure and the same methods, with 16-octet addresses and a
prefix of at most 128. A static address that is not usable in the sense
PROP_IPV6_STATE defines, link-local, multicast,
loopback, or unspecified, is refused with STATUS_INVALID_ARGUMENT; a
static address that is usable but already held on the link is accepted
and reported as IP_CONFLICT.
IP_METHOD_AUTO means router advertisements, and DHCPv6 where the
router asks for it. Which of those produced an address is not reported,
because a host has nothing to do with the difference.
A device MUST hold a stable address and MUST report only stable addresses. It MAY additionally use temporary addresses for the traffic it originates, which is the arrangement RFC 8981 describes for a host that is reached at one address and reaches out from others, and those are never reported: they exist to rotate, and a reported address is one somebody wrote down.
PROP 4901: PROP_IPV6_ADDRESSES
- Type: Multiple-Value, Read-Only
- Has Item Length Prefix: Yes
- Asynchronous Updates: Yes
- Required:
CAP_IPV6 - Post-Reset Value: what is in effect; empty when the family is not
IP_READY
The usable IPv6 addresses the device holds, and the default routers it has selected. Each item is a kind octet and a kind-defined body:
| Kind | Name | Body |
|---|---|---|
| 0 | IPV6_ADDRESS | 16-octet address, 1-octet prefix length |
| 1 | IPV6_ROUTER | 16-octet router address |
A set rather than one address, because an IPv6 interface normally holds several, a global one and a unique-local one from separate prefixes, say, and which of them the device uses as a source depends on where the packet is going. There is no one answer to “the device’s address”, only “the addresses the device is reachable at”, which is what a host displaying or dialing it needs. Stable addresses only, per the configuration above. The link-local address is not among them, for the reason it does not make the family ready.
The router items are the stack’s default router list, every router it currently retains from those advertising, and none when there is none. A list rather than one, because a stack keeps several and may send to different destinations through different ones, so no single router describes the routing.
The prefix length is the one the address’s assignment carried: the
advertised prefix an autoconfigured address was formed from, or the
prefix written for a static one. An address assigned by DHCPv6 reports
128, and 128 means the assignment carried none rather than that the
link is a /128: DHCPv6 assigns addresses, not prefixes, and the
on-link prefixes a router advertises alongside are routing state that
is not encoded here.
A device bounds the set to what its stack holds, which for an embedded stack is a few addresses and a few routers, and the bound MUST keep the complete value inside one frame on every transport the device exposes. Which entries a stack keeps once a network offers more than it can hold is the stack’s business.
The device MUST publish it whenever the reported set changes: a
prefix renumbered, a router replaced or expired, an address added or
withdrawn. Router and prefix lifetimes are independent of one another
and of the state, so this property moves while PROP_IPV6_STATE stands
still, and a host that only watched the state would not learn.
PROP 4902: PROP_IP_DNS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: Yes
- Asynchronous Updates: No
- Required:
CAP_IPV4orCAP_IPV6 - Item Form: a 4-octet IPv4 or 16-octet IPv6 resolver address
- Remove Selector: the address
- Post-Reset Value: Empty, or restored from saved state
The resolvers the device is to use. Empty, the default, means the ones the network provided, by DHCP or router advertisement. Non-empty, these replace the network’s rather than join them, because a host that configured resolvers meant those, and a merged set would be neither what it wrote nor what the network offered.
An item of any other length, or one that is not a unicast address,
unspecified, multicast, broadcast, or loopback, is refused with
STATUS_INVALID_ARGUMENT. An IPv6 resolver MAY be link-local, as
one advertised by a home router commonly is, and an IPv4 one MUST
NOT be. A device bounds the set, SHOULD hold at least two, and
refuses past its capacity with STATUS_NOMEM.
The set is unordered, as every multi-value property is, and a device asks whichever it likes first.
Static addressing without this is a device that cannot resolve a name, which is why the two are written together.
PROP 4903: PROP_IP_RESOLVERS
- Type: Multiple-Value, Read-Only
- Has Item Length Prefix: Yes
- Asynchronous Updates: Yes
- Required:
CAP_IPV4orCAP_IPV6 - Item Form: as
PROP_IP_DNS - Post-Reset Value: the resolvers in use; empty when there are none
What the device is resolving with right now: the configured set when
PROP_IP_DNS is non-empty, otherwise what the network handed it, and
empty when neither has given it anything.
The device MUST publish it whenever the set changes, for the same
reason the addresses are published: a router advertisement carries
resolvers with lifetimes of their own and can add one, replace one, or
withdraw one with a zero lifetime, all without the family leaving
IP_READY. Bounded as
PROP_IPV6_ADDRESSES is, to what the stack
holds and to one frame; a stack that keeps two or three resolvers is
the norm, and a network offering more than that is offering more than
the device will ask.
Chiefly a diagnostic, and the one that distinguishes “the device is on the network and cannot resolve the server’s name” from every other way a tunnel fails to come up.
Alongside the Link
On a device whose link is Wi-Fi, the two subsystems meet at exactly one
property on each side: PROP_WIFI_LINK
says whether there is a link, and the two family states say what each
family has made of it. The address and resolver properties are read
after one of those has moved and followed thereafter, since each
publishes its own changes.
The stack follows the link. While the link is anything but up,
every enabled family is IP_NO_LINK and both address properties are
empty. When the link comes up, each enabled family goes to
IP_WAITING and then, as the network answers, to IP_READY; a static
family skips the wait except for the duplicate-address check, which
ends in IP_READY or IP_CONFLICT. When the link drops, everything
goes back to IP_NO_LINK and the addresses empty with it, learned
resolvers included. A roam to another access point of the same network
is the same link and does not disturb the stack: a device MUST NOT
release its addresses over a roam.
Publication order is link first, then stack, in both directions. On
the way up, PROP_WIFI_LINK carrying WIFI_LINK_UP, then each
family’s state for each transition as it happens; on the way down,
PROP_WIFI_LINK leaving WIFI_LINK_UP, then each family’s state
carrying IP_NO_LINK. A host that sees IP_READY reads the addresses
it wants and keeps them current from their own notifications; one that
sees IP_WAITING persist while the link is up knows the problem is the
network and not the radio; and one that sees IP_CONFLICT knows it is
the configuration.
Disabling the link reports IP_NO_LINK, not IP_DISABLED. The
second is the family’s own switch, written in its configuration, and a
host that turned the radio off did not turn IPv6 off.
Configuration is staged in any order. These configuration
properties are writable while the link is down and while the radio is
disabled, and they are ordinary CMD_PROP_SET targets, so a static
commissioning is one CMD_PROP_MULTI_SET where CAP_CMD_MULTI is
present: PROP_IPV4_CONFIG, PROP_IP_DNS, PROP_WIFI_NETWORK,
PROP_WIFI_ENABLED, in that order, then CMD_SAVE. The network insert
still stands apart, being an insert.
Synchronizing on attach adds the family states to the link’s read:
PROP_WIFI_ENABLED, PROP_WIFI_NETWORK, PROP_WIFI_LINK,
PROP_IPV4_STATE, and PROP_IPV6_STATE, in one
CMD_PROP_MULTI_GET, where a family the device lacks comes back as the
STATUS_PROP_NOT_FOUND entry the multi-get already provides for.
Between them a host knows whether the device is configured, associated,
and addressed without a second round trip.
Reconfiguring a live device is a write and a watch. The device
applies the new configuration at once, the family leaves IP_READY and
arrives wherever the new configuration leads, IP_READY again by way of
IP_WAITING, IP_CONFLICT for a static address somebody else holds, or
IP_DISABLED, and a host that changed a static address learns the old
one is gone by the same notification everyone else does.
The host name the device presents to DHCP, and to DHCPv6 where it
runs, SHOULD be derived from PROP_DEV_NAME, folded to a valid
label, so that the device can be found on a router’s client list under
the name its operator gave it.
What survives what. The three configuration properties are saved
and revert with CMD_RST; the five live ones follow, in the sense the
Wi-Fi chapter’s table defines.
CMD_RST on a device whose snapshot matches its live IP configuration
leaves a lease in place; one that reverts a static address releases and
re-applies, with the transitions published.
Over the node management binding, all eight are device-domain: an administrator may read every one and write the three configuration properties, the other five being read-only for everybody. That binding carries no unsolicited notifications, so the read-then-follow flow does not apply across the mesh: an administrator that wants a current view of addresses or resolvers reads them again, and the family states tell it when that is likely to be worth doing. An administrator who writes a static address that is wrong for the network has stranded a bridge just as surely as one who disabled the link, and the same warning belongs in the same place.
Security Considerations
The IP stack trusts its network the way every client does. A rogue DHCP server or router advertisement on the LAN can hand the device a bad address, a bad route, or a resolver that lies. Nothing at this layer defends against that, and nothing needs to, because what rides on the address authenticates its far end itself: the bridge tunnel pins a key, and a redirected tunnel fails to open rather than opening to the wrong party.
Not Specified
- A reachability test. Command-shaped, and unnecessary: a bridge client reports whether its tunnel is up, and that is the reachability anyone cares about.
- A DHCP renew request. The same argument. A lease is the stack’s to manage, and a host that could force a renewal could not observe anything by it that the address property does not already publish.
- Time from the network. It belongs to the time capability, which
would gain a trust switch shaped like
PROP_GNSS_TIME_TRUSTif it takes SNTP. - A second interface. A selector for a later revision. The device’s
own access point is not one of these interfaces; see
PROP_WIFI_AP_CONFIG. - mDNS. Letting a phone find the device by name is a service the device offers rather than a property of its stack, and it waits for whatever first needs it.
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, as MUST
CMD_REBOOT without CAP_REBOOT and
CMD_ANNOUNCE without CAP_ADVERT;
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 |
PROP_DEV_MODEL (4) and PROP_UPTIME (6) are the two properties that are
neither always required nor capability-gated. Firmware built for a specific
board SHOULD implement the first, and a device with a monotonic clock
SHOULD implement the second; anything else omits them. A host discovers
both by asking.
Every other property is gated by a capability. A device that does not
advertise the capability does not implement the property, and rejects it
with STATUS_PROP_NOT_FOUND or STATUS_UNIMPLEMENTED.
Status and Reset Reporting
A device MUST implement PROP_LAST_STATUS as the failure channel for
every command, using the most specific applicable
status code, and MUST emit a
reset code asynchronously after every reset.
A device that cannot determine the cause reports
STATUS_RESET_UNKNOWN rather than omitting the notification.
Optional Subsystems
Everything else in this specification is a capability. Each grants the commands and properties defined in its chapter, and a device MUST NOT advertise a capability without also advertising the capabilities it requires.
| Subsystem | Capabilities |
|---|---|
| Radio Control beyond the required properties | CAP_PHY_LORA, CAP_PHY_DUTY_LIMIT, CAP_STATS |
| 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, CAP_MAC_BACKHAUL |
| Wi-Fi | CAP_WIFI_SCAN, CAP_WIFI, CAP_WIFI_AP |
| IP Connectivity | CAP_IPV4, CAP_IPV6 |
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 | — |
| 16 | CMD_REBOOT | Host->Device | CAP_REBOOT |
| 19 | CMD_ANNOUNCE | Host->Device | CAP_ADVERT |
| 21 | CMD_PROP_MULTI_GET | Host->Device | CAP_CMD_MULTI |
| 22 | CMD_PROP_MULTI_SET | Host->Device | CAP_CMD_MULTI |
| 23 | CMD_PROP_ARE | Device->Host | CAP_CMD_MULTI |
| 24 | CMD_SESSION_RESET | Device->Host | — |
Command identifiers are 7-bit; 17–18, 20, and 25–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 | — |
| 4 | PROP_DEV_MODEL | Get | — |
| 5 | PROP_CAPS | Get | — |
| 6 | PROP_UPTIME | 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 |
| 50 | PROP_MAC_BACKHAUL | Get, Set | CAP_MAC_BACKHAUL |
| 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 |
| 83 | PROP_IDENT_LOCATION | Get, Set | CAP_IDENT |
| 84 | PROP_IDENT_ALTITUDE | Get, Set | CAP_IDENT |
| 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 |
| 104 | PROP_HOST_MUTED_CHANNELS | Get, Set, Insert, Remove | CAP_HOST_RX_QUEUE |
| 105 | PROP_HOST_MUTED_PEERS | Get, Set, Insert, Remove | 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 |
| 4832 | PROP_STAT_TX_PACKETS | Get, Set | CAP_STATS |
| 4833 | PROP_STAT_TX_CHANNEL_BUSY | Get, Set | CAP_STATS |
| 4834 | PROP_STAT_RX_PACKETS | Get, Set | CAP_STATS |
| 4835 | PROP_STAT_RX_BAD_CRC | Get, Set | CAP_STATS |
| 4836 | PROP_STAT_RX_NON_UMSH | Get, Set | CAP_STATS |
| 4837 | PROP_STAT_RX_ACCEPTED | Get, Set | CAP_STATS, CAP_REPEATER |
| 4838 | PROP_STAT_FORWARDED | Get, Set | CAP_STATS, CAP_REPEATER |
| 4839 | PROP_STAT_FORWARD_DROPPED | Get, Set | CAP_STATS, CAP_REPEATER |
| 4840 | PROP_STAT_FORWARD_CANCELLED | Get, Set | CAP_STATS, CAP_REPEATER |
| 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 |
| 4871 | PROP_BLE_ENABLED | Get, Set, Is | CAP_BLE |
| 4872 | PROP_BLE_BOND_COUNT | Get, Set, Is | CAP_BLE |
| 4873 | PROP_BLE_LINK | Get, Is | CAP_BLE |
| 4874 | PROP_BLE_PAIRING | Get, Set, Is | CAP_BLE |
| 4880 | PROP_WIFI_ENABLED | Get, Set, Is | CAP_WIFI |
| 4881 | PROP_WIFI_NETWORKS | Get, Set, Insert, Remove | CAP_WIFI |
| 4882 | PROP_WIFI_NETWORK | Get, Set, Is | CAP_WIFI |
| 4883 | PROP_WIFI_SCANNING | Get, Set, Is | CAP_WIFI_SCAN |
| 4884 | PROP_WIFI_SCAN_RESULTS | Get, Is, Inserted | CAP_WIFI_SCAN |
| 4885 | PROP_WIFI_LINK | Get, Is | CAP_WIFI |
| 4886 | PROP_WIFI_RSSI | Get | CAP_WIFI |
| 4887 | PROP_WIFI_MAC | Get | CAP_WIFI |
| 4896 | PROP_IPV4_STATE | Get, Is | CAP_IPV4 |
| 4897 | PROP_IPV4_CONFIG | Get, Set | CAP_IPV4 |
| 4898 | PROP_IPV4_ADDRESS | Get, Is | CAP_IPV4 |
| 4899 | PROP_IPV6_STATE | Get, Is | CAP_IPV6 |
| 4900 | PROP_IPV6_CONFIG | Get, Set | CAP_IPV6 |
| 4901 | PROP_IPV6_ADDRESSES | Get, Is | CAP_IPV6 |
| 4902 | PROP_IP_DNS | Get, Set, Insert, Remove | CAP_IPV4 or CAP_IPV6 |
| 4903 | PROP_IP_RESOLVERS | Get, Is | CAP_IPV4 or CAP_IPV6 |
| 4912 | PROP_WIFI_AP_ENABLED | Get, Set, Is | CAP_WIFI_AP |
| 4913 | PROP_WIFI_AP_CONFIG | Get, Set | CAP_WIFI_AP |
| 4914 | PROP_WIFI_AP_STATE | Get, Is | CAP_WIFI_AP |
| 4915 | PROP_WIFI_AP_CLIENTS | Get, Is, Inserted, Removed | CAP_WIFI_AP |
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 |
| 48 | CAP_MAC_BACKHAUL | Tethered Host Services |
| 49 | CAP_CMD_MULTI | Framing and Common Semantics |
| 50 | CAP_BLE | BLE Binding |
| 51 | CAP_REBOOT | Framing and Common Semantics |
| 52 | CAP_STATS | Radio Control |
| 53 | CAP_WIFI_SCAN | Wi-Fi |
| 54 | CAP_WIFI | Wi-Fi |
| 55 | CAP_IPV4 | IP Connectivity |
| 56 | CAP_IPV6 | IP Connectivity |
| 57 | CAP_WIFI_AP | Wi-Fi |
| 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 | 22 | STATUS_NOT_PERMITTED |
| 9 | STATUS_PARSE_ERROR | 23 | STATUS_CHANNEL_NOT_FOUND |
| 10 | STATUS_IN_PROGRESS | 32 | STATUS_DUTY_LIMIT |
| 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, bit 2 RX_FLAG_SELF_TX | Extended Recv Metadata |
| Session reset reasons | 0 attached, 1 CMD_RST, 2 CMD_RESTORE | CMD_SESSION_RESET |
| Wi-Fi security modes | 0 WIFI_SEC_OPEN, 1 WIFI_SEC_OWE, 2 WIFI_SEC_WPA2, 3 WIFI_SEC_WPA3, 4 WIFI_SEC_WPA, 5 WIFI_SEC_WEP, 6 WIFI_SEC_WPA2_ENT, 7 WIFI_SEC_WPA3_ENT, 8 WIFI_SEC_WPA3_ENT_192 | PROP_WIFI_NETWORKS |
| Wi-Fi link states | 0 WIFI_LINK_DOWN, 1 WIFI_LINK_CONNECTING, 2 WIFI_LINK_UP | PROP_WIFI_LINK |
| Wi-Fi link reasons | 0 WIFI_REASON_NONE, 1 WIFI_REASON_NOT_FOUND, 2 WIFI_REASON_AUTH, 3 WIFI_REASON_REJECTED, 4 WIFI_REASON_LOST, 5 WIFI_REASON_OTHER | PROP_WIFI_LINK |
| Access point states | 0 WIFI_AP_DOWN, 1 WIFI_AP_UP | PROP_WIFI_AP_STATE |
| IP family states | 0 IP_DISABLED, 1 IP_NO_LINK, 2 IP_WAITING, 3 IP_READY, 4 IP_CONFLICT | PROP_IPV4_STATE |
| IP configuration methods | 0 IP_METHOD_DISABLED, 1 IP_METHOD_AUTO, 2 IP_METHOD_STATIC | PROP_IPV4_CONFIG |
| IPv6 address item kinds | 0 IPV6_ADDRESS, 1 IPV6_ROUTER | PROP_IPV6_ADDRESSES |
ULCP over BLE
This chapter defines the normative binding of ULCP (see Framing and Common Semantics) onto Bluetooth Low Energy. It covers the tethered case only: one host device driving its own companion radio over a BLE connection, exactly as it would over UART or USB-CDC.
Using BLE as a shared local bearer—nearby devices exchanging UMSH frames over BLE as peers, or reaching the UMSH network through a BLE-LoRa bridge—is a separate bearer design and is out of scope here. See BLE As A Local Bearer for the design space. This chapter reserves identifier space for that future work (see UUID Allocation) but does not specify it.
ULCP is transport-agnostic: frames are carried opaquely and unchanged. This binding replaces only the framing layer. HDLC-Lite framing (flags, escaping, and the FCS) is not used over BLE; ATT already provides reliable, ordered, integrity-protected delivery, and frame boundaries are recovered by the segmentation scheme below.
GATT Frame Transport
This section defines a generic, service-agnostic pattern for carrying delimited frames over GATT. The ULCP GATT Service (see ULCP GATT Service) instantiates it; a future local-bearer service may instantiate it independently.
A service using this pattern exposes a pair of characteristics:
| 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.
Capabilities
| Code | Name | Requires | Grants |
|---|---|---|---|
| 50 | CAP_BLE | — | A Bluetooth transport whose reachability the device can turn on and off, and which reports what is on it: PROP_BLE_ENABLED, PROP_BLE_LINK |
A device implementing this binding MAY advertise CAP_BLE. Not
advertising it means the transport is always reachable while the device
is powered, which is what every device did before the capability
existed; it does not mean the device has no BLE.
CAP_BLE is the only capability this binding defines, and everything
else about a transport is discovered by asking for it. A device that
manages its own bonds answers PROP_BLE_BOND_COUNT and
PROP_BLE_PAIRING; one whose bonds are reachable only by a gesture at
the device itself answers STATUS_PROP_NOT_FOUND to both.
This is deliberate. A capability is worth a code when a host would otherwise have to guess, and here it would not: the refusal is a complete answer, arrives in the same exchange the host was already making, and is a case the host must handle regardless—any property may be refused by firmware older than the host that asks. Splitting the transport into finer capabilities would buy a host nothing it cannot learn in the reply it is already waiting for, at the cost of a second claim that can disagree with the first.
Reachability
PROP 4871: PROP_BLE_ENABLED
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_BLE - Value Type: BOOL
- Post-Reset Value: 1 (true), or restored from saved state
Whether the device is reachable over this transport. Cleared, the device MUST stop advertising and MUST drop any host attached over BLE; set again, it advertises as it did before. Bonds are untouched in both directions, so a bonded host reconnects without pairing again.
It says nothing about the radio itself. A device MAY power the controller down behind this and MAY leave the whole stack running; what the property promises is reachability, which is what an operator turning it off is asking about. Claiming the radio is off would be a claim most platforms cannot honor—a vendor stack that cannot be torn down at runtime is common—and a property that lies in the direction of “more private than it is” is the wrong one to guess at.
Asynchronous for the same reason PROP_GNSS_ENABLED is: a device
MAY offer this as a control the operator can reach, and a switch
someone can flip is a value that moves without the host asking. A
device that flips it locally MUST publish the new value like any
other transition the host did not command—which, when it is being
cleared, is the last thing the attached host hears.
The post-reset value is true. A device unreachable by default is a device that cannot be configured by the host that would make it reachable again, and on most hardware the only other way in is the menu on the front of it.
PROP 4873: PROP_BLE_LINK
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_BLE - Value Type: UINT8
- Post-Reset Value: what the transport is doing
| Value | Name | Meaning |
|---|---|---|
| 0 | BLE_LINK_NONE | Nothing is connected over Bluetooth |
| 1 | BLE_LINK_CONNECTED | A central holds a connection but has not attached |
| 2 | BLE_LINK_ATTACHED | A host is attached and running ULCP over Bluetooth |
How far the transport has got with whoever is on the other end of it. A device MUST publish the new value when it changes: a host arriving or walking away is a transition nobody asked for, and one that a watching administrator would otherwise have to poll for.
Connected and attached are separate values because they are separate facts. A central can hold the device’s connection without ever subscribing to the ULCP notification characteristic—a stalled pairing, an operating system reconnecting a bond in the background, or a host that simply occupies the slot—and a device with one peripheral connection is unreachable by anyone else while that lasts. Reporting that as “nobody is here” would describe a device that is in fact unavailable.
Read over BLE the value is always BLE_LINK_ATTACHED, because the
session asking is the session it reports. The property earns its keep on
the other bindings: over a serial transport and over
Node Management it is the only way to ask whether
someone is on the device’s Bluetooth right now.
Like the bond count, it is live transport state: NOT part of the
saved snapshot, and CMD_RST MUST NOT change it. A host does not
disconnect because a reset was performed on the device it is attached
to.
It says what the transport is doing, never with whom. Identifying the connected host would leak the same association the bond count withholds.
Bond Management
PROP 4872: PROP_BLE_BOND_COUNT
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_BLE - Value Type: UINT8
- Post-Reset Value: the number of bonds the device holds
How many hosts are currently bonded. A device MUST report the count its durable bond store holds, and MUST publish the new value when it changes—enrollment and eviction both happen without the host asking, so a host that was not told would have to poll.
A device that does not manage its own bonds answers
STATUS_PROP_NOT_FOUND, which is how a host learns that neither half of
bond management is available to it.
The count is live transport state, not configuration: it is NOT part
of the saved snapshot, and CMD_RST MUST NOT change it. A protocol
reset returns protocol state to its post-reset values, and a bond is
neither protocol state nor something a reset deletes.
It says how many hosts are enrolled, never which. A device that named its bonded hosts to whoever asked would leak the association the pairing ceremony exists to protect, and the count is what an operator deciding whether to clear bonds actually needs.
Writing Zero: Forgetting Every Host
Writing 0 deletes every stored bond, the pairing PIN, and the pairing
failure lockout, then enters pairing mode.
Zero is the only value a host may write, and any other MUST be
answered STATUS_INVALID_ARGUMENT. Bonds are enrolled one pairing
ceremony at a time and evicted by the device, so no other count names a
state a device could be put into: a host asking for three bonds is not
describing anything the device could do.
The device MUST NOT answer before the deletion is durable, and MUST drop the deleted bonds from any live in-memory bond table as well as from durable storage—a bond forgotten on flash but still held in RAM would keep working until the next boot. It MUST then enter pairing mode: a device that has forgotten every host it trusts and is not accepting new ones is reachable by nothing.
The write is answered like any other, with the property’s value: a
CMD_PROP_IS carrying 0 once the deletion is durable. Sent over BLE,
that answer is the last thing the sender hears, because the bond that
carried it is among the bonds deleted; the reply MUST be emitted
before the connection is dropped.
Clearing the PIN alongside the bonds is deliberate. A PIN outliving the hosts it was set for would leave a device that has forgotten everyone still demanding a secret the operator may no longer have, recoverable only by a local wipe.
PROP_BLE_ENABLED does not gate this write, which
is where it parts company with
PROP_BLE_PAIRING. Bonds are durable state rather
than reachability: a device with the transport turned off still holds
them and still counts them, and deleting them is exactly as meaningful
there as it is with a host connected. The pairing mode the deletion
leaves behind is then a window onto a transport that is down, which
PROP_BLE_PAIRING reports as closed like any
other.
Forgetting every host is a write rather than a command, and rather than a reset-class one, because the count is already the state it changes. A command would have had to be answered by a status saying what the property could say by quoting itself, and a host reading the count back is asking the same question the answer already contains.
PROP 4874: PROP_BLE_PAIRING
- Type: Single-Value, Read-Write
- Asynchronous Updates: Yes
- Required:
CAP_BLE - Value Type: BOOL
- Post-Reset Value: whether the window is open
Whether pairing mode is active. Writing 1 opens a
window, so an unbonded host may pair without a physical gesture at the
device; writing 0 closes one. The window is a property rather than a
command because it is a state with more ways out than in—the write, a
timeout, a completed bond—and only a property can be closed again,
read back, and reported moving on its own.
The device MUST publish the new value on any transition the writer did not just command: expiry and a new bond both close the window with no host asking, and a physical gesture at the device opens one.
A write of 1 answers STATUS_INVALID_STATE when no window can open:
the device is locked out after repeated pairing failures, or Bluetooth
is off (PROP_BLE_ENABLED is 0)—nothing can
pair through a transport that is down, and a device MUST NOT report
a window nothing can walk through. A full bond store is NOT a
refusal: enrollment at capacity evicts rather than refuses (see
Bond Management), so a device with a full store
opens the window like any other. A write of 0 MUST succeed: there
is no state in which a window refuses to shut.
A device that does not manage its own bonds answers
STATUS_PROP_NOT_FOUND, like the bond count.
Like the bond count and the link, the window is live transport state:
NOT part of the saved snapshot, and CMD_RST MUST NOT touch
it.
Security
ULCP is a privileged interface: an attached host commands transmission with arbitrary content, timing, and power under the operator’s regulatory responsibility, observes all traffic metadata the radio receives, and can deny service to the legitimate host. On serial transports this interface is implicitly protected by physical possession of the device. The BLE binding MUST provide at least an equivalent barrier, and its cryptographic strength MUST NOT fall below that of UMSH’s identity layer (approximately 128-bit, bounded by Curve25519; see Security & Cryptography).
Pairing Requirements
- Pairing MUST use LE Secure Connections (LESC). Legacy pairing MUST be rejected; Devices SHOULD operate in Secure Connections Only mode.
- Bonding is REQUIRED. The device MUST NOT attach a host over an unbonded link.
- The Frame In and Frame Out characteristics, including the Frame Out CCCD, MUST be readable and writable only over an encrypted link keyed by a stored LESC bond. Access over any other link MUST be refused with the appropriate ATT security error.
LESC pairing (P-256 ECDH) meets the 128-bit strength requirement. The remaining risk is man-in-the-middle interception during the pairing ceremony itself, which the following requirements bound.
Pairing Mode
Except as provided for OOB pairing below, the device accepts pairing requests from unbonded devices only while in pairing mode; at all other times such requests MUST be rejected. A configured pairing PIN selects the association model but does not bypass the pairing-mode requirement.
Entering pairing mode:
- While the device holds no bonds, it SHOULD enter pairing mode automatically at power-on 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 either a
deliberate physical gesture distinct from normal power-on—for
example, holding the user button through power-on until the device
signals that pairing mode is active—or a write of
1toPROP_BLE_PAIRINGfrom an authorized session.
Pairing mode MUST end when any of the following occurs:
- a new bond completes;
- an already-bonded host establishes an encrypted connection;
- an implementation-defined timeout expires.
The device SHOULD give a perceptible indication (LED pattern, tone, or display) while pairing mode is active.
A physical-presence-gated ceremony reduces the pairing trust decision
to possession of the device—the same property that protects the
serial transports. A command from an already-authorized session is the
same decision made by someone who has already passed that ceremony: an
attached host holds a bond, and a mesh administrator is listed in
PROP_DEV_ADMINS, which is itself set through an attached session. What
the gesture proves about a person standing at the device, authorization
proves about a party that was admitted earlier; a party that can already
administer the device gains nothing by opening a window it could open by
walking over.
Association Models
- Devices with a display and a confirmation input SHOULD use an authenticated association model (numeric comparison, or passkey display) for new bonds.
- Devices without a display, and with no pairing PIN configured, use Just Works, accepted only in pairing mode. This model is unauthenticated; the pairing-mode gesture is the entire trust decision.
- Devices with a pairing PIN configured (see Pairing PIN Configuration) use LESC Passkey Entry with the configured PIN as a static passkey. New bonds still MUST be accepted only while in pairing mode. The device MUST count consecutive passkey authentication failures—pairing attempts that fail the LESC confirm-value or DHKey check—since power-on; the counter resets on a successful pairing or a power cycle. Rejections that never reach passkey authentication (legacy-pairing attempts, pairing refused outside pairing mode, malformed pairing requests) MUST NOT increment the counter, so they cannot be used to lock out pairing remotely. After a small limit (MUST NOT exceed 5; 3 RECOMMENDED), the device MUST reject all further pairing attempts until it is power cycled.
- Devices MAY additionally support LESC Out-of-Band pairing (for example, OOB data conveyed by a QR code affixed to or displayed by the device). OOB pairing is authenticated and MAY be accepted at any time. The conveyance and provisioning of OOB data is out of scope for this document.
The failed-attempt lockout is load-bearing, not defensive polish: LESC Passkey Entry discloses the passkey one bit per protocol round, so an active attacker learns roughly one PIN bit per failed pairing attempt, and a passive eavesdropper on one successful pairing learns the entire PIN. A static passkey therefore provides bounded, not absolute, authentication: the lockout caps active extraction at a few bits per power cycle performed by the operator, and operators SHOULD change the PIN if a pairing exchange may have been observed.
Pairing PIN Configuration
PROP 4864: PROP_BLE_PAIRING_PIN
- Type: Single-Value, Write-Only
- Asynchronous Updates: No
- Required: OPTIONAL (meaningful only on devices exposing this transport)
- Value Type: UINT32_LE, or empty
- Units: LESC passkey, decimal 0–999999
- Post-Reset Value: Persisted
Sets the static passkey used by the configured-PIN association model
above. Writing an empty value clears the PIN, returning the device to
the Just Works model. Values outside 0–999999 fail with
STATUS_INVALID_ARGUMENT.
As an exception to the usual CMD_PROP_SET behavior, a successful
set of this property is acknowledged with CMD_PROP_IS for
PROP_LAST_STATUS carrying STATUS_OK and the command’s TID; the
device MUST NOT emit CMD_PROP_IS for this property itself. Success
MUST NOT be reported before the new value is in effect for
subsequent pairing attempts and, where the device supports
persistence, durably stored; a value that cannot be applied or stored
is reported with an appropriate error status and leaves the previous
PIN state unchanged.
The PIN persists across resets and power cycles. It is write-only:
CMD_PROP_GET for this property MUST fail with
STATUS_UNIMPLEMENTED and MUST NOT disclose the value or whether
a PIN is configured.
Because this property is only reachable through an attached session, it is always protected by the transport that carried it: physical possession on serial transports, or an existing bonded LESC link on BLE.
Bond Management
- Devices MUST provide a local mechanism to delete stored bonds. The
mechanism is implementation-specific. Deletion MUST NOT be
invocable over an unauthenticated path; a device that manages its own
bonds additionally accepts a write of zero to
PROP_BLE_BOND_COUNT, whose authorization is the session’s own (see Administrative Authorization). - Devices MAY limit the number of stored bonds. A full bond store MUST NOT cause pairing to be refused: when a new bond is enrolled while the store is full, the device MUST evict the least-recently-used bond to make room. Bond-store capacity MUST NOT appear as a term in the pairing admission decision.
- A device that evicts by recency MUST record use: the recency order MUST be updated when a bonded host establishes an encrypted connection, not only when a bond is created or refreshed. A store ordered only by enrollment evicts the wrong bond.
- A device that evicts a bond from durable storage MUST also drop it from any live in-memory bond table, so the evicted peer cannot continue to reconnect as bonded for the remainder of the power cycle.
Refusing enrollment at capacity would be the more restrictive-looking choice and is the wrong one: it turns a full store into a state from which the device can only be recovered by a local wipe, while enrollment already requires physical presence and the eviction victim is by construction the bond that has gone longest without connecting.
Removing one specific bond while retaining the others is not expressible through this protocol, which is also why zero is the only count a host may write. The count written to zero forgets every host at once, and that is the operation an operator reaching for it wants: the case that motivates it is a device whose paired hosts are no longer trusted or no longer known, and enumerating bonds so one could be named would mean reporting which hosts a device has met.
Administrative Authorization
Any retained secure bond may administer the device. There is no per-host privilege distinction: a host that has completed the pairing ceremony and holds a stored bond has the same authority as any other, for as long as its bond is retained.
Equivalently, possession of a serial transport confers the same
authority. The serial transports have no cryptographic admission step
at all, so a host that can open the port is attached; this is the
MUST in Pairing Requirements read the other
way—BLE is required to reach the barrier that physical possession
already provides, not to exceed it.
The consequences worth stating plainly:
- Enrollment, not authorization, is the gate. Reaching the pairing ceremony requires pairing mode, and pairing mode requires physical presence (see Pairing Mode).
- Administering a device does not make the administering host that device’s tethered host, and MUST NOT cause the device to adopt it as one. See Local Control Protocol for the distinction between administrative and tethered attach.
- All transports implementing this protocol MUST apply the same rules. A device MUST NOT grant a capability over one transport that it withholds over another.
Layering Note
BLE link security protects the transport. It does not alter the UMSH security model: MAC-layer keys remain on the host, frames crossing this link remain UMSH ciphertext where UMSH encrypts them, and a compromised device still cannot impersonate host identities. Conversely, future ULCP extensions that provision keying material to the device (see Security Boundary) MUST NOT be carried over a link that does not meet the requirements of this section.
ULCP: Minimal Protocol
Note
This chapter has been superseded and is retained for reference. Its content now lives in Framing and Common Semantics, Radio Control, and Frame Transport. What a device is required to implement—the question this chapter’s minimal/full split used to answer—is stated in Minimum Requirements, and every numeric identifier is listed in the Command and Property Index.
The UMSH Local Control Protocol (ULCP) is inspired by the Spinel protocol from OpenThread, but it is not Spinel and does not aim for wire compatibility with it. The protocol assumes reliable, in-order delivery of frames, as well as a way to assert flow control. The framing mechanism depends on the underlying transport:
- Asynchronous serial links (UART, USB-CDC) use HDLC-Lite, exactly as used by Spinel.
- BLE uses the GATT frame transport defined in ULCP over BLE.
The specific subset of the protocol documented here is the minimal set needed to configure and use a LoRa radio. All other concerns are left out to make it easier to implement this initial step. The full protocol is a strict superset of this document, adding receive filtering, inbound queueing, key provisioning, and acknowledgement delegation.
In this document, the device is the side that owns the transceiver and the host is the side that attaches to it over the local link (see Local Control Protocol).
Data Representation
Spinel, being a low-level protocol between two devices which are likely to have a little-endian architecture, uses little-endian representations exclusively for all integers smaller than four bytes. For implementation convenience, values larger than four bytes (EUI64, IPv6 addresses, etc.) are stored as they are traditionally represented (typically, but not always, big-endian).
Packed Unsigned Integers
Certain types of integers, such as command or property identifiers, usually have a value on the wire that is less than 127. However, in order to not preclude the use of values larger than 255, we would need to add an extra byte. Doing this would add an extra byte to all packets, which can add up in terms of bandwidth. To address this, Spinel uses Packed Unsigned Integers, or PUIs.
The PUI format used in Spinel is based on the unsigned integer format in EXI, except that we limit the maximum value to the largest value that can be encoded in three bytes. The maximum value that can be encoded is 2,097,151.
For all values less than 127, the packed form of the number is simply a single byte which directly represents the number. For values larger than 127, the following process is used to encode the value:
- 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 the session-scoped
properties PROP_MAC_PROMISCUOUS and PROP_MAC_BACKHAUL. Session state
is reset to defaults on every attach. Resetting it never affects radio
operation.
Device Domain
State that belongs to the device itself, independent of which host is attached:
- the device identity keypair (independently persisted; never part of the saved snapshot—see (#saved-state))
- the device identity’s channel keys ((#prop-dev-channel-keys)) and peer list ((#prop-dev-peers))
- the RF configuration (
PROP_PHY_*), 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.
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 |
| 50 | PROP_MAC_BACKHAUL | Get, Set | Point-to-point link to the device’s node (session-scoped) |
| 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.
Session-scoped: it reverts to false on every attach.
PROP 49: PROP_SAVED
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required:
CAP_SAVE - Value Type: UINT8
Whether a saved snapshot is in effect (see (#saved-state))—that is, whether the device is armed for autonomous operation across a power cycle— and, when the answer is qualified, how:
| 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 50: PROP_MAC_BACKHAUL
- Type: Single-Value, Read-Write, Session-Scoped
- Asynchronous Updates: No
- Required:
CAP_MAC_BACKHAUL - Value Type: BOOL
- Post-Attach Value: 0 (false)
When false, the host and the device’s own node are two listeners on one shared medium. Both transmit through the same radio and both hear what it receives, so a frame from one reaches the other only by way of some third node that repeats it.
When true, the host is instead a point-to-point neighbor of the device’s node:
- A frame the host sends on
STR_PHY_RAWis delivered to the node as though the node had heard it, and is never transmitted directly. It spends no airtime and is not subject to the duty-cycle limit. - Frames the node transmits are delivered to the host with
RX_FLAG_SELF_TXset, subject to the usual receive filtering. - Frames the radio receives are not delivered to the host at all. The node is the only thing listening to the medium.
Traffic between the host and the mesh therefore crosses the device’s repeater: hop accounting, duplicate suppression, and forwarding policy are the node’s, applied to the host’s traffic as to anyone else’s. A device whose repeater is disabled still delivers the host’s frames to its own identities, but carries nothing onward.
PROP_MAC_PROMISCUOUS composes with this: it removes receive filtering
from what the host is delivered, which in this mode is the node’s
transmissions.
Session-scoped: it reverts to false on every attach.
PROP 64: PROP_DEV_KEY
- Type: Single-Value, Read-Only
- Asynchronous Updates: No
- Required:
CAP_DEV_IDENTITY - Value Type: 32 octets, or empty
- Post-Reset Value: Persisted
The Ed25519 public key of the device identity (see (#identity-model)). The public key is also emitted as the success response when the private key is installed or generated (see (#prop-dev-private-key)).
An empty value means the device has no device identity. A conforming device does not report one in normal operation—an identity is generated at first boot if none is stored—so hosts SHOULD treat an empty value as a fault to surface rather than as an invitation to provision one.
Frames addressed to the device identity are processed by the device itself. They are additionally delivered or queued to the host only if they independently match the host’s receive filtering (see (#receive-filtering)).
PROP 65: PROP_DEV_PRIVATE_KEY
- Type: Single-Value, Write-Only
- Asynchronous Updates: No
- Required:
CAP_DEV_IDENTITY - Value Type: 32 octets, or empty
- Post-Reset Value: Persisted
Installs or generates the device identity private key. An identity always exists already (see (#identity-model)), so both forms replace one:
- Setting a 32-octet value installs it as the device identity’s Ed25519 private key. This is the recovery path—moving a known repeater’s identity onto replacement hardware—not a commissioning step.
- Setting an empty value commands the device to generate a fresh private key entirely on-device from its cryptographically secure random number generator. On-device generation is RECOMMENDED over installation, since a generated key never exists anywhere but the radio.
In both cases, success is reported by emitting CMD_PROP_IS for
PROP_DEV_KEY—carrying the resulting public key—with the
command’s TID. The private key itself is never emitted. Success MUST NOT
be reported before the new identity is in effect and durably stored.
Replacing an existing device identity is permitted; implementations
SHOULD treat the device identity’s peer list and channel keys as
still valid, since they are not derived from the identity key.
This property is write-only: CMD_PROP_GET MUST fail with
STATUS_UNIMPLEMENTED and MUST NOT disclose the value or whether an
identity is configured (use PROP_DEV_KEY for that).
The device identity is not part of the saved snapshot (see
(#saved-state)): it is durably persisted as soon as it is installed or
generated, and it is changed only by another set of this property or by
CMD_CLEAR. CMD_RESTORE never reverts it—though it does read the
identity a snapshot was taken under, and refuses to enable the PHY when
it does not match (see (#cmd-restore)).
Replacing the identity takes effect for the property surface immediately and for anything the device built around the old key at the next boot. The old key stops being one the device claims at once, so a device running a device node MUST stop originating traffic under it rather than continue until the reboot.
Installing a private key is subject to the same transport security requirements as all key provisioning (see (#provisioning-security)).
PROP 66: PROP_DEV_CHANNEL_KEYS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_DEV_IDENTITY - Item Form: 32 octets (the channel key)
- Digest Form: 2 octets (the derived channel identifier)
- Remove Selector: the 32-octet channel key
- Post-Reset Value: Empty, or restored from saved state
The set of channel keys belonging to
the device identity—channels the radio’s own node participates in
(for example, a site-infrastructure management channel). These are
independent of the host domain: they survive host replacement and are
distinct from PROP_HOST_CHANNEL_KEYS.
For each key the device derives the 2-byte
channel identifier and the
channel’s K_enc/K_mic
(see Multicast Packet Keys). The
digest form reported for each entry is that derived channel identifier;
the key itself is never read back.
Device channel keys do not create implicit host receive filters: frames on these channels are consumed by the device node and reach the host only through the host’s own filtering.
PROP 67: PROP_DEV_PEERS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: No
- Asynchronous Updates: No
- Required:
CAP_DEV_IDENTITY - Item Form: 32 octets (the peer’s Ed25519 public key)
- Remove Selector: the 32-octet public key
- Post-Reset Value: Empty, or restored from saved state
The device identity’s peer list: the set of peer public keys the device node recognizes and may communicate with securely. Because the device holds the device identity’s private key, it performs its own key agreement (Unicast Key Agreement) for these peers—no symmetric keys are provisioned, and the entries contain no secret material.
How the device node uses this list (management access control, secure diagnostics, and so on) is application behavior outside the scope of this protocol.
PROP 68: PROP_DEV_NAME
- Type: Single-Value, Read-Write
- Asynchronous Updates: No
- Required:
CAP_DEV_NAME - Value Type: 1–64 octets of UTF-8, without U+0000
- Post-Reset Value: Implementation-defined default, or restored from saved state
The operator-assigned, human-readable name of the physical device. It is independent of the device and host cryptographic identities and MUST NOT be derived from a bonded host or other host-domain state.
Setting the property changes the live name immediately. Like other ordinary
device-domain configuration, it is included in a CMD_SAVE snapshot but is
not independently persisted merely by being set. Applications and transports
that present the device to a person SHOULD use this value when practical.
They MAY shorten it to fit a constrained presentation, but MUST NOT
split a UTF-8 code point when doing so.
The name is intentionally public metadata. Operators should assume that any value used in discovery advertisements can be observed by nearby devices.
PROP 69: PROP_BATTERY
- Type: Single-Value, Read-Only
- Asynchronous Updates: Yes
- Required:
CAP_BATTERY - Value Type: Battery status snapshot (see below), or empty
- Post-Reset Value: Current measurement, or empty if reporting is unsupported
A device advertising CAP_BATTERY has a battery capable of powering its
operation and recognizes this property. The capability does not require the
hardware to support reporting any measurement: an implementation that cannot
report battery status at all answers CMD_PROP_GET successfully with an
empty value.
A non-empty value is a snapshot of the battery measurements the platform supports, taken as one measurement event:
| 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, 96 octets
- Digest Form: 32 octets (the peer’s public key)
- Remove Selector: the 32-octet peer public key
- Post-Reset Value: Empty
Pairwise symmetric key material provisioned for specific already-known peers of the host identity. The item form is:
+---------------------+-----------+-----------+
| PEER_PUBLIC_KEY | K_ENC | K_MIC |
+---------------------+-----------+-----------+
32 B 32 B 32 B
Figure: Peer key entry item form
Where PEER_PUBLIC_KEY is the peer’s Ed25519 public key and K_ENC and
K_MIC are the stable pairwise keys for the (host, peer) pair, derived by
the host as described in
HKDF Inputs for Unicast. The device
never derives these itself—it cannot, because it does not hold the host’s
private key.
As an exception to the usual CMD_PROP_INSERT duplicate rule, inserting an
entry whose PEER_PUBLIC_KEY matches an existing entry replaces that
entry. Replacement updates only the stored key material: the peer’s replay
baseline (see (#ack-delegation)) and any frames already queued from that
peer are unaffected, since both are keyed by the peer’s identity rather
than by the key values. The digest form is the peer public key alone:
K_ENC and K_MIC are never read back.
Provisioned peer keys let the device authenticate inbound unicast and blind unicast from those specific peers and acknowledge it on the host’s behalf (see (#ack-delegation)). They grant no capability regarding any other peer, and do not allow the device to establish new pairwise relationships.
PROP 99: PROP_HOST_RX_FILTERS
- Type: Multiple-Value, Read-Write
- Has Item Length Prefix: Yes
- Asynchronous Updates: No
- Required:
CAP_HOST_FILTER - Item Form: Structure
- Remove Selector: the full item
- Post-Reset Value: Empty
The explicit receive filter table. Each item is a filter entry:
+-------------+----------------------+
| FILTER_TYPE | FILTER_VALUE ...
+-------------+----------------------+
1 B type-specific
Figure: Filter entry format
| 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.
Extended Recv Metadata
The Recv metadata of STR_PHY_RAW
(see Metadata for Recv) may carry two
further trailing fields:
RX_FLAGS(u8): Delivery 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.RX_FLAG_SELF_TXBit 2: The device transmitted this frame itself and is delivering a copy of it.RX_RSSIandRX_SNRMUST carry their unsupported sentinels: a transmitter measures nothing.- All other bits: RESERVED, transmitted as zero
RX_AGE(u32, little-endian): Seconds elapsed between reception of the frame and its delivery to the host. Zero for live delivery.
As with the existing metadata fields, the metadata may be truncated at any field boundary; absent fields are treated as zero. A live delivery with nothing to flag MAY therefore omit these fields entirely, which keeps the encoding byte-compatible with the minimal protocol.
Acknowledgement Delegation
With PROP_HOST_AUTO_ACK enabled, the device acknowledges qualifying inbound
frames so that senders’ retransmission logic is satisfied while the host is
away. The device MUST transmit a MAC ack for a received frame if and only
if all of the following hold:
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 = AES-SIV (RFC 5297) with AES-256 |
| 34 | 16 | MIC | full 16-byte S2V output V |
| 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-V2"
okm = HKDF-SHA256(ikm, salt, info, 64)
K_mic = okm[0..32]
K_enc = okm[32..64]
Exporters MUST use at least m = 19456 KiB, t = 2, p = 1, and SHOULD use m = 65536 KiB, t = 3, p = 1 where device memory allows. Importers MUST honor the parameters carried in the header, but MAY refuse artifacts whose parameters exceed a local resource ceiling (for example m > 1048576 KiB, t > 32, or p > 4) to avoid resource-exhaustion attacks through crafted headers.
Encryption and Authentication
The payload is protected with the same construction used for packets (see Encrypted Packets), with the full 16-byte MIC and the envelope header as associated data:
- Compute the full 16-byte synthetic IV
V = S2V(K_mic, S1, S2)per RFC 5297 §2.4, whereS1is the envelope header andS2is the payload plaintext.Vis stored as the MIC. - Form the CTR IV by clearing the top bit of bytes 8 and 12 of
V(RFC 5297 §2.6). - Encrypt the payload using AES-256-CTR with
K_encand that IV.
Because the MIC is always full-length here, this is exactly
AEAD_AES_SIV_CMAC_512 (RFC 5297) with key K = K_mic || K_enc and the
envelope header as the single associated-data component.
To import, derive the keys, decrypt the ciphertext, recompute V over
the header and recovered plaintext, and compare it to the stored MIC in
constant time. On mismatch the importer MUST report a single generic
failure: a wrong passphrase and a corrupted or forged artifact are
deliberately indistinguishable.
Payload
The payload is a CBOR (RFC 8949) map using unsigned-integer keys. Definite-length encoding MUST be used. Deterministic encoding is not required; the envelope, not the payload encoding, provides integrity.
| 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 set of radios in different places, joined by an authenticated tunnel over a reliable stream transport such as the internet. It specifies the tunnel wire protocol, how a participant attaches its radio, and the small number of decisions the tunnel itself makes about what it carries.
Caution
The cautions in Routing Overview § Bridging apply in full: internet bridges cannot be relied upon in an emergency and can waste airtime with non-local chatter. This appendix exists so that bridges which are deployed anyway behave predictably and conservatively.
Model
A bridge consists of one bridge server and one or more bridge clients. Together they form a hidden radio layer: a medium that joins segments which are nowhere near each other.
- Each participant fronts a ULCP device of its own, attached as a tethered host in backhaul mode. The host is therefore not on the shared medium at all—it is a point-to-point neighbor of the device’s own node.
- The server’s interfaces are its own radio, when it has one, plus one interface per connected client, plus any host interfaces it offers. The server’s radio is a participant like any other; nothing distinguishes it but the absence of a tunnel.
- The server copies each frame it receives to its other interfaces. It makes no forwarding decisions and holds no mesh state.
The bridge has no identity on the mesh. It appears in no route, answers to no address, and originates no traffic. The nodes that carry bridged traffic are the ones behind the participants’ radios—and, where a host interface is offered, the host on the far side of it—each holding its own identity.
A Crossing Is Two Repeater Hops
Because every participant is a point-to-point neighbor of its device’s node, the node’s repeater is the bridge’s forwarding policy—hop accounting, duplicate suppression, region and signal policy, all applied to bridged traffic exactly as to anything else the node hears.
A packet crosses in four steps:
- A node on the ingress segment transmits. The participant’s device hears it off the air and its node forwards it by the ordinary forwarding procedure: duplicate check, flood hop accounting, trace prepend with a real signal measurement, and a transmission on that same segment.
- That transmission is delivered to the attached host, which writes it to the tunnel. What crosses is the ingress node’s output, already rewritten—not the frame as it was first heard.
- The server copies it to its other interfaces.
- Each receiving participant hands the frame to its own device’s node, which receives it as a packet carrying no measurements and forwards it by the same procedure: a second duplicate check, a second hop accounting, a trace signal entry recording that nothing was measured, and no contention window, since nobody else heard it.
Two consequences follow, and deployments should plan for both. A crossing spends two flood hops rather than one, so a packet sent with a budget of three arrives on the far segment with one. And the ingress node’s transmission in step 1 is an ordinary repeat on the segment the packet came from, which is exactly what the previous hop listens for: the bridge satisfies forwarding confirmation without transmitting anything for the purpose.
Everything the node transmits reaches its host, so everything the node transmits crosses: its repeats, its beacons, its acknowledgements, and its own application traffic. That is the node participating in the mesh the bridge has joined it to.
Tunnel Echoes
A frame transmitted by an egress node in step 4 is delivered to that node’s own host in the same way, written to the tunnel, and copied onward. Each of the other participants’ nodes then receives a packet it has already forwarded, and its duplicate cache ends it there.
This costs one extra round of tunnel messages per crossing and no airtime at all. It is what makes the arrangement safe: the duplicate caches at the participants, not any rule in the bridge, are what stop a frame circulating. A deployment MUST NOT attach a participant whose device does not suppress duplicates.
Radio configuration is local to each participant: a client configures its own device, and the tunnel provides no mechanism for managing a remote participant’s radio.
Tunnel Transport
Connection and Authentication
The tunnel is a TLS 1.3 connection over TCP; the client connects to the server. Earlier TLS versions MUST NOT be negotiated.
Peer authentication uses either external pre-shared keys (TLS 1.3 PSK handshakes) or mutually authenticated certificates with pinned or locally trusted roots. Each client SHOULD hold a distinct credential: it is how the server identifies a client for policy and rate limiting, and it allows one client to be revoked without re-keying the rest.
A deployment MAY use an Ed25519 node identity as its certificate key,
each side pinning the peer’s public key rather than a certificate. The
pinned key MUST then be held against the TLS 1.3 CertificateVerify
signature—proof that the peer possesses the identity, independent of
anything the certificate claims—rather than against the certificate’s
contents. This gives every tunnel credential a UMSH address, so a
participant’s credential can later serve as a mesh-addressable identity
for management without re-keying.
The tunnel carries no version of its own. Participants SHOULD offer
the ALPN protocol identifier umsh-bridge/1, so that an incompatible
future revision fails the handshake instead of misparsing frames.
Message Framing
The tunnel is a stream of HDLC-Lite frames, exactly as ULCP uses on asynchronous serial links (see Framing and Common Semantics). The frame check sequence is redundant beneath TLS but is retained so implementations can reuse their existing framing code unchanged.
There is no message header. A non-empty frame contains one
STR_PHY_RAW structure and nothing
else; an empty frame is a keepalive. Because the payload is
exactly the ULCP stream structure, a participant relays bytes without
parsing them:
- Client to server: something the client’s node transmitted—the
body of the
CMD_STR_RECVthat delivered it, written unmodified,RX_FLAG_SELF_TXand all. - Server to client: a frame to hand to the client’s node, passed as
the body of a
CMD_STR_SENDonSTR_PHY_RAW.
The metadata a frame carries across the tunnel describes how it was received, and a transmit request needs metadata of its own. The participant that hands a frame to its device therefore MUST replace the accompanying metadata with transmit metadata, and SHOULD do so only at that point, so that the received metadata—including any buffered-frame age—remains available to the staleness rule below for as long as the frame is in flight.
A frame that is malformed, or larger than the receiving participant is prepared to transmit, is discarded.
Keepalive and Reconnection
A participant writes a bare flag octet (0x7E) whenever it has sent
nothing for a keepalive interval—10 seconds is a reasonable default—
and closes the connection once it has received nothing for an idle
timeout, by default 30 seconds. The two directions are independent.
Empty frames are the idle fill of HDLC-Lite and a conforming decoder already discards them, so the keepalive needs no message of its own. Two properties make it sufficient. Liveness is measured in received octets rather than decoded messages, so a discarded flag still counts as activity. And because the keepalive is written by the participant’s own relay logic, a peer whose relay has wedged stops emitting it even while its TLS connection stays open.
A stalled local transmit path is not visible to the idle timer, since the peer keeps talking. It appears as tunnel-queue backpressure instead, which is bounded below.
Frames queued for a tunnel that fails or backs up are stale by definition: participants MUST bound their tunnel queues, SHOULD drop the oldest frames first under backpressure, and MUST discard queued frames when a connection is re-established rather than flushing them into the new session.
Staleness is enforced by the sender, and no age accompanies a frame on the wire. A participant SHOULD discard a frame rather than write it once the frame is older than a configured limit—ten seconds is a reasonable default—counting any device-side queueing reported through buffered-frame metadata. A frame that old describes a mesh that has moved on.
Radio Attachment
Each participant attaches to its device as an ordinary tethered host,
without resetting it, and sets
PROP_MAC_BACKHAUL to true. This
requires the device to advertise CAP_MAC_BACKHAUL, which in turn
requires CAP_REPEATER.
A device that does not advertise CAP_MAC_BACKHAUL MUST NOT be
attached to a bridge on the shared medium instead. A host on the medium
transmits directly: its frames reach the air without passing any node’s
duplicate suppression, hop accounting, or forwarding policy, which are
the whole of what makes a crossing safe.
A participant SHOULD also set
PROP_MAC_PROMISCUOUS to true. In
backhaul mode this widens what the host is delivered from those of the
node’s transmissions that pass its receive filtering to all of them—
which for a device with a provisioned host domain is the difference
between carrying the node’s repeats and silently dropping them. A device
with no host domain provisioned filters nothing, so this is a safeguard
rather than a requirement.
Both properties are session-scoped and revert on every attach, so both must be re-asserted after each reconnection to the device.
A participant MUST NOT write
PROP_MAC_REPEATER_ENABLED. Whether a device repeats is
persisted, device-domain configuration belonging to whoever provisioned
it. A participant whose device has its repeater disabled is a leaf:
its node is reachable across the bridge and its own traffic crosses, but
nothing is carried onward from its segment, and nothing arriving from
the bridge reaches the air. This is a legitimate deployment, and an
implementation SHOULD report it rather than treat it as a fault.
Transmission requires the device to advertise
CAP_WRITABLE_RAW_STREAM. Participants
SHOULD use confirmed transmissions (non-zero TID) so that refusals
are observed rather than silent, and bridged transmissions MUST NOT
set TX_FLAG_NODUTY: the device’s duty-cycle enforcement is the backstop
against a bridge that would otherwise consume a segment’s airtime budget.
A backhauled hand-off crosses a wire. It contends for no channel, spends
no airtime, and is not charged against the duty limit—the airtime is
spent later, and accounted to the node, if the node decides to transmit.
What a hand-off can meet is a node whose receive queue is full, which is
reported as STATUS_CCA_FAILURE for want of a better code. A participant
SHOULD wait briefly and offer the frame again, and MUST continue
draining its device’s receive path while it waits: draining the node’s
output is what makes room in its input.
Whether a handed-off frame reaches the air is the node’s decision. A duplicate it has already forwarded, or one whose flood budget is spent, ends at the node. That is the forwarding policy working, and a participant MUST NOT treat it as a failed hand-off or retry it.
No participant provisions node logic for the bridge: there is no bridge identity to provision. A participant’s device is configured as whatever node its owner intends it to be.
Host Interfaces
A server MAY offer host interfaces: interfaces whose far end is not a participant fronting a segment but a single host, attached directly to the bridge’s hidden medium. The host runs its own MAC and holds its own identity, exactly as it would behind a radio; what it lacks is a radio, and therefore a node of its own standing between it and the medium.
The server presents a ULCP device on such an interface. That device’s
radio is the bridge: what the host transmits is relayed by the
relay procedure like anything else, and what the
relay copies to the interface is delivered to the host as a reception.
The device MUST NOT advertise CAP_MAC_BACKHAUL, because there is
no node for a backhaul to connect the host to, and a participant
MUST NOT attach to one: a bridge’s forwarding policy is its
participants’ repeaters, and a host interface has none to offer.
A crossing to or from a host spends one flood hop rather than the two of § A Crossing Is Two Repeater Hops, since no repeater stands between the host and the medium. What makes this safe is unchanged: a host’s frames reach the air only where a participant’s node transmits them, and that node applies the whole forwarding procedure—duplicate suppression, hop accounting, region and signal policy, duty enforcement—to them as to anything else it hears.
Frames delivered to a host MUST carry no signal measurements. The measurement that accompanied such a frame across the tunnel describes a reception on another segment by another radio, and reporting it here would attribute it to a reception that did not happen. This is the same fact the crossing model already records as a trace signal entry saying nothing was measured.
The server MUST rate-limit each host interface. A host spends no airtime of its own, which removes the natural bound a radio imposes, while every frame it injects is transmitted by every participant’s node.
The transport carrying ULCP to a host is a local binding and is outside this appendix. Whatever it is, it is a ULCP transport like any other and its attach and detach are the establishment and closure of the underlying connection. A deployment MUST NOT expose that binding beyond a host it trusts as it would trust a device on the end of a cable: reaching it is an RF presence on every segment the bridge touches, and a binding that meets Provisioning Security by physical possession also carries whatever key provisioning and administrative authority the device offers.
Relay Procedure
For a frame arriving on interface I, the server:
- Discards it if it has grown stale (see Keepalive and Reconnection).
- Charges it against I’s traffic limits, and discards it if the limit is spent.
- Applies the exit clamp, if one is configured.
- Copies it to every interface except I, subject to any configured per-interface-pair rules.
There is no step that reads what the frame means. The server MUST NOT suppress duplicates, account for hops, match source routes, prepend trace entries, or apply signal-quality thresholds; those belong to the participants’ nodes, which apply them to bridged traffic and local traffic alike. A frame the server cannot parse is carried unchanged like any other.
Clients apply nothing at all beyond the framing rules above: a client relays bytes between its radio and the tunnel.
Traffic Limits
The server SHOULD rate-limit per client. An authenticated but misbehaving client is the realistic failure mode of a bridge, and the duty ledgers of the devices at the far end should be the backstop, not the policy.
A limit counts everything the client’s node transmits, which includes that node’s repeats of frames the bridge itself handed it. A crossing therefore charges a little against every participant, not only the one whose segment the traffic came from. Budgets should be set with that in mind.
Exit Clamp
A deployment MAY configure the server to clamp the remaining flood budget of frames passing through it. This is the one thing the server does that depends on a frame’s contents, and it exists so that an operator can pull a bridge’s reach in quickly without reconfiguring every device behind it.
When a clamp of n is configured, the server rewrites FHOPS_REM to n
for any frame whose FHOPS_REM exceeds it. The
flood hop count is dynamic routing
metadata excluded from the MIC, so the packet remains authentic—this is
the same field a repeater decrements. The server MUST NOT alter
FHOPS_ACC, which is a record of hops already taken; MUST NOT add
the field to a frame that carries none, since a sender that omitted it
meant the frame not to be flooded onward; and MUST NOT discard a
frame it cannot parse, which is carried unchanged.
The clamp is off by default. A crossing already spends two flood hops, which bounds a bridged flood without any configuration, and a clamp narrows the reach of every deployment behind the bridge at once. A clamp of 0 stops bridged floods at the egress segment while leaving traffic addressed to the participants’ own nodes unaffected.
Acknowledgements Across a Bridge
Acknowledgements cross as ordinary traffic. A destination’s MAC ack is transmitted by its node, reaches that node’s attached host, crosses the tunnel, and is forwarded back toward the originator by the repeaters along the way—two hops for the crossing, as for anything else.
The round trip therefore costs four flood hops of the sender’s budget: two out and two back. A sender whose budget does not cover the round trip receives no acknowledgement even though the packet arrived. Where an exit clamp is configured, the returning ack is clamped too, and route failure recovery cannot repair that: the restored flood is clamped the same way.
Source-routed hops spend nothing from FHOPS, so explicit routes cross
bridges at any depth. Ack-requesting traffic that crosses a bridge
SHOULD either carry a
trace-route option—the
participants’ nodes record themselves in it, which is what makes the
reversed trace routable—or be sent along a known source route.
Operational Guidance
- Budget for two hops. Traffic expected to cross a bridge needs a flood budget that covers the crossing at both ends, and twice that if an acknowledgement is expected back.
- Co-located clients. Two clients whose radios share a segment cause every copied frame to be handed to that segment twice. Their nodes’ duplicate caches keep it from being transmitted twice, but the tunnel traffic is real; per-interface-pair rules are the place to exclude one from the other’s fan-out.
- Region and signal policy. These are configured on each participant’s device, where the decision to transmit is actually made. A bridge whose segments sit in different regions relies on each node’s own region matching.
- Leaf participants. A participant whose device does not repeat still reaches the whole bridge and is reachable from it. This is the right configuration for a device that should benefit from a bridge without spending its segment’s airtime on other people’s traffic.
Security Considerations
TLS provides tunnel authentication, integrity, and replay protection; the shared secret or pinned credential is the sole admission control. UMSH frames are already end-to-end authenticated and encrypted at the MAC layer, so the tunnel’s confidentiality mainly shields routing metadata—hints, options, traffic volume—from path observers.
Using a node identity as a tunnel credential does not let the two
protocols’ signatures be confused for one another: the TLS 1.3
CertificateVerify payload is domain-separated by a fixed 64-octet
padding prefix and context string that no UMSH signed structure begins
with.
A compromised client credential is equivalent to granting the attacker an RF presence on every segment the bridge touches: it can inject arbitrary well-formed frames and replay captured ones. It cannot forge other nodes’ authenticated traffic. The damage is bounded by the same mechanisms that bound a hostile local transmitter, and by the same parties: every frame it injects is subject to the duplicate suppression, hop accounting, region policy, and duty-cycle enforcement of the node that would have to transmit it. Per-client rate limits, an exit clamp, and revocation of the client’s credential bound it further.
A backhauled host is off the medium and is delivered only what its own node transmits, so promiscuous delivery grants it nothing beyond the repeats it exists to carry. Operators should still treat the server as privileged infrastructure: it observes the metadata of every frame every participant’s node transmits.
Security Considerations
This chapter consolidates the security properties, limitations, and implementation guidance that are distributed throughout the specification. It is intended as a reference for implementers and reviewers evaluating UMSH’s security posture.
Threat Model
UMSH is designed for a shared radio medium where any device in range can observe and inject packets. The threat model assumes:
- Passive eavesdroppers can observe all traffic on the channel, including packet timing, size, hint values, and frame counters.
- Active attackers can inject, replay, modify, or selectively drop packets.
- Compromised nodes may leak their long-term private keys, channel keys, or both.
UMSH does not assume a trusted infrastructure, a reliable transport, or a synchronized clock.
What UMSH Protects Against
Eavesdropping. When encryption is enabled, payload content is protected by AES-256-CTR keyed with material derived from ECDH (unicast) or the channel key (multicast). An observer without the relevant key cannot recover plaintext.
Forgery. All authenticated packets carry a MIC computed with S2V (AES-CMAC). An attacker who does not possess the encryption and authentication keys cannot produce a valid MIC. The MIC size determines the forgery resistance—from 2^-32 (4-byte MIC) to 2^-128 (16-byte MIC).
Replay attacks. Monotonically increasing frame counters allow receivers to detect and reject replayed packets. The backward window and MIC cache provide tolerance for out-of-order delivery without weakening replay protection.
Nonce misuse. The AES-SIV construction derives the CTR IV from the key, associated data, and plaintext, so accidental nonce reuse (e.g., due to a buggy counter implementation) does not produce the catastrophic plaintext leakage that would occur with AES-GCM or raw AES-CTR. In the worst case, an attacker can detect when two packets carry identical associated data and plaintext—the keys and other traffic remain uncompromised.
Long-term key compromise (with PFS). If a PFS session was active and the ephemeral keys were properly erased, traffic from that session cannot be retroactively decrypted even if the long-term private keys are later compromised.
What UMSH Does Not Protect Against
Traffic analysis. A passive observer can see packet timing, frequency, size, hint values, frame counters, and flood hop counts—all in the clear. This reveals communication patterns (who is active, how often, rough network topology) even when payloads are encrypted. Hint values are stable for a given identity, enabling long-term tracking of a node’s activity.
Multicast sender impersonation. Multicast authentication is based on the shared channel key. Any node possessing the key can construct a valid packet with any claimed source address. 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 Session Lifetime.
Constant-Time MIC Verification
MIC comparison must use constant-time comparison (e.g., a fixed-iteration XOR-and-OR loop) rather than memcmp or similar short-circuiting functions. A timing side channel in MIC verification allows an attacker to incrementally guess MIC bytes by measuring response time.
Public Key Validation
Implementations must reject malformed Ed25519 public keys before converting them to X25519 form. Accepting a malformed key can produce a low-order X25519 point, resulting in a shared secret of zero—which would cause all pairwise keys to be identical across different peers. See Ed25519 to X25519 Conversion.
Reserved Bits
Packets with non-zero reserved bits in the Security Control Field must be dropped. Accepting unknown bit patterns could indicate a protocol version mismatch or a malformed packet; processing them risks undefined behavior.
Metadata Exposure
Even with encryption enabled, the following information is visible to a passive observer. Which fields are present depends on the packet type:
| 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 AES-SIV (RFC 5297) rather than AES-GCM. AES-GCM is catastrophically vulnerable to nonce reuse: a single repeated nonce leaks the authentication key and allows forgery of arbitrary messages. On a mesh network where counter management is distributed across many independent nodes and persistence across reboots is not guaranteed, nonce reuse is a realistic failure mode. AES-SIV degrades gracefully—nonce reuse reveals only whether two packets are identical, without compromising keys or enabling forgery. Deterministic SIV-style alternatives built on other primitives, such as ChaCha20-Poly1305-SIV, were considered, but none is as mature or as widely analyzed as RFC 5297. See the FAQ.
AES-256 over AES-128
UMSH’s asymmetric layer—Ed25519 identities and X25519 key agreement—provides roughly 128-bit classical security, so AES-128 would be a matched choice against classical adversaries, and a cheaper one on low-power hardware. UMSH uses AES-256 anyway, because the symmetric layer is the one part of the protocol that can meaningfully survive a quantum adversary.
No standardized post-quantum key exchange or signature scheme fits within a LoRa frame budget, so the asymmetric layer cannot be hardened against Shor’s algorithm. Recovering a private key that way, however, requires the complete public key. Most UMSH traffic exposes only a 3-byte hint, and blind unicast conceals even a full first-contact source key inside the channel-encrypted address block. A deployment that avoids exposing full public keys and leans on shared channel keys therefore retains meaningful—though reduced—security against a quantum adversary: physical compromise of any channel member defeats it, but a purely over-the-air attacker is left facing the symmetric layer alone. That fallback is only as strong as the symmetric primitives, and Grover’s algorithm halves a symmetric key’s effective strength: AES-128 would be reduced to roughly 64-bit post-quantum strength, while AES-256 remains far out of reach. Choosing AES-256 preserves the symmetric fallback at the cost of extra cycles and code size on constrained devices.
Stable Keys over Ratcheting
UMSH uses stable pairwise keys rather than a ratcheting protocol. Ratcheting provides forward secrecy per-message but requires synchronized state between sender and receiver. On a lossy, high-latency mesh where packets are routinely dropped, duplicated, or delivered out of order, ratchet state can desynchronize—potentially requiring expensive resynchronization exchanges over a slow radio link. UMSH’s stable keys combined with per-packet counter and salt inputs provide per-packet IV uniqueness without requiring synchronized state. Optional PFS sessions provide forward secrecy when needed, without imposing ratcheting’s fragility on all traffic. See the FAQ.
Single Keypair for Signing and Key Agreement
UMSH uses a single Ed25519 keypair per node for both identity (signing) and key agreement (via X25519 conversion). Standard guidance recommends separate keys, but the alternative would require distributing an additional 32-byte X25519 public key per identity and cryptographically binding it to the Ed25519 key. On a ~255-byte LoRa frame, this overhead is significant. The Ed25519/X25519 conversion is a well-understood, deterministic mapping over birationally equivalent curves, used by Signal’s X3DH and libsodium. See Ed25519 to X25519 Conversion.
Channel-Specific Considerations
Named Channel Security
Named channels derive their key from a human-readable name via HKDF-Extract. Anyone who knows (or guesses) the name can derive the key. Named channels should be treated as public—they provide a shared namespace, not confidentiality. Long, high-entropy names offer practical obscurity but should not be relied upon for security.
Emergency Channel Integrity
The EMERGENCY channel requires unencrypted transmission, full source key (S=1), and 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
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 S2V output 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 MIC verification will fail). The sender can retransmit with S=1 to provide the full key.
How does a MAC Ack get routed back to the original sender?
MAC acks are end-to-end: the final destination generates the ack, not any intermediate repeater. The ack is routed back to the original sender using whatever routing state the destination has learned—typically a source route derived from the inbound packet’s trace 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, while the AES-SIV construction provides an additional safety margin against nonce misuse.
When forward secrecy is needed, UMSH provides PFS sessions—a two-message handshake where both nodes exchange ephemeral node addresses and communicate using session-specific keys for an agreed duration. PFS sessions add no per-packet overhead once established, and the private keys for the ephemeral addresses are erased when the session ends. This provides perfect forward secrecy without the fragility of continuous ratcheting.
What happens if a 2-byte channel identifier collides across different channel keys?
The 2-byte channel identifier is a hint, not a unique identifier. If two different channel keys happen to produce the same 2-byte channel ID, a receiver configured with both keys will attempt to process the packet with each candidate key. Only the correct key will produce a valid MIC, so the wrong candidate will be rejected during authentication. The cost is wasted computation, not incorrect behavior. With 2 bytes the collision probability is 1 in 65536, which is higher than a 4-byte hint but still negligible for deployments with a small number of configured channels.
Why use AES-SIV instead of AES-GCM?
AES-GCM is catastrophically vulnerable to nonce reuse—repeating a nonce with the same key completely breaks both confidentiality and authenticity. In a mesh network, nonce management is difficult: nodes may reboot and lose counter state, clocks may not be synchronized, and packets may be retransmitted. AES-SIV (RFC 5297) computes its CTR IV from the key, associated data, and plaintext, so there is no independent caller-supplied nonce to misuse: repeating all inputs produces an identical packet and reveals only the repetition itself. That is exactly the failure mode UMSH wants under counter loss—graceful, not catastrophic. See Encrypted Packets.
How does “deliver to a region, then flood” work?
A sender can include both a source-route option and a flood hop count in the same packet. The source-route directs the packet through specific repeaters, and as each repeater forwards, it removes its own hint. Once all source-route hints are consumed, the packet transitions to flood-based forwarding bounded by FHOPS_REM. This allows targeted delivery to a specific area of the mesh followed by a local flood—useful when searching for a node in a known geographic region without flooding the entire network. See Routing Implications.
Can UMSH support anonymous requests, similar to MeshCore’s ANON_REQ mechanism?
Yes. A node can generate an ephemeral Ed25519 keypair, set the S flag, and use the ephemeral public key as the source address for a single request, then discard the private key immediately afterward. The recipient performs ECDH with the ephemeral public key as normal, encrypts a response to it, and sends it back. The requester’s long-term identity is never revealed. This pattern also provides forward secrecy for the exchange: once the ephemeral private key is discarded, the session cannot be decrypted even if the requester’s long-term key is later compromised. No dedicated packet type is required.
Does UMSH support automatic route learning?
Yes. A node that wants to learn a source route to a peer sends any flooded packet (unicast, broadcast, or beacon) with the trace-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.17.0 and its primary source code and documentation.
Note
This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!
Identity and Addressing
Both protocols use Ed25519 public keys as node identities and perform X25519 ECDH for pairwise key agreement.
| 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 source and destination hashes in regular encrypted unicast payloads, while routing paths support configurable 1-, 2-, or 3-byte node hashes. A dedicated ANON_REQ packet type carries the full 32-byte sender public key for first-contact or anonymous exchanges. The tradeoff is that MeshCore saves bytes per address field in the common case, but requires a special packet type for any situation where the full key must be transmitted.
Packet Structure
| 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-type code points (13 assigned; via 4-bit field) |
| Routing info | CoAP-style options (source route, trace route, region, RSSI/SNR thresholds) | Path field (up to 64 bytes; 1–3-byte hashes), 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-256-CTR (AES-SIV, RFC 5297) | AES-128-ECB |
| Authentication | S2V (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 32-byte encryption and 32-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 (160 entries); timestamps at application layer |
The cryptographic gap is substantial:
-
AES-128-ECB is a textbook-insecure mode: it uses no IV or nonce, and identical plaintext blocks produce identical ciphertext blocks, leaking structural information about the payload. AES-CTR with a synthetic IV (as used by UMSH) does not have this weakness.
-
2-byte MAC truncation in MeshCore means an attacker has a 1-in-65536 chance of forging a valid MAC per attempt, which is marginal for a protocol where an attacker can observe and replay packets at will. UMSH’s 16-byte MIC provides a forgery probability of 2^-128.
-
No key separation in MeshCore means the same bytes of the ECDH shared secret serve as both the AES key and the beginning of the HMAC key. UMSH derives independent keys via HKDF with domain-specific labels, which is the standard practice for preventing cross-protocol or cross-purpose key reuse.
-
MAC verification timing: MeshCore’s
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.
Region-Scoped Flooding
Although both protocols carry a 16-bit value to scope flood forwarding, those values are derived and evaluated very differently.
An UMSH region code is a stable two-byte identifier for a region. It is derived from the configured region name according to the Region Code Encoding, independently of any particular packet. A sender places one or more region-code options in the routing metadata, and a repeater decides whether to forward by comparing those values directly with its configured region codes. The payload remains opaque: changing the payload does not change the region code or require the repeater to perform any payload-dependent calculation.
A MeshCore transport code is packet-specific rather than a stable identifier for a region. For a public region, MeshCore first derives a 16-byte transport key from the region name: the first 16 bytes of SHA-256 over the #-prefixed name. Despite its name, this is not an encryption key; anyone who knows the public region name can derive it. For each packet, the transport code is the first 16 bits of:
HMAC-SHA256(transport_key, payload_type || payload)
MeshCore remaps the reserved results 0x0000 and 0xFFFF to 0x0001 and 0xFFFE, respectively. The routing path and transport-code fields are not included in the HMAC input, so repeaters can extend the path without changing the code. Changing either the payload type or payload bytes does change the code.
When a MeshCore repeater receives a transport-scoped packet, it iterates through the region entries allowed for that routing mode, obtains up to four candidate transport keys for each entry, recomputes the HMAC for each candidate, and compares the result with the packet’s first transport code. It forwards only after finding a match. Thus, MeshCore does not decrypt the payload to make this decision, but its forwarding layer does cryptographically process the payload once per candidate key. The work grows with both the number of candidate keys and the payload length. UMSH instead performs fixed two-byte comparisons against routing metadata.
This payload-dependent transport-code mechanism was introduced in MeshCore commit 03fc9490.
Both protocols support automatic route learning, but through different mechanisms. UMSH uses a trace-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) | Yes (no cleartext source; claimed sender name is encrypted) |
| 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 | Group sender and content concealed; no blind-unicast equivalent |
Both protocols conceal the claimed sender of encrypted group traffic from observers who do not possess the channel key. UMSH encrypts the source address inside the multicast ciphertext. MeshCore group packets have no cleartext source field and place the self-asserted sender name inside the ciphertext. MeshCore does not cryptographically authenticate that group sender name and does not define an equivalent of UMSH blind unicast, which conceals both unicast endpoints from non-members.
Both protocols support anonymous first-contact requests, but through different mechanisms. UMSH uses an ephemeral keypair as the source address with the S flag set—no dedicated packet type is needed. MeshCore defines a specific ANON_REQ payload type that carries the full 32-byte sender public key.
Multicast
| 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 S2V MIC | 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 cleartext source; claimed sender name encrypted |
Both protocols share the fundamental limitation that symmetric-key multicast cannot authenticate individual senders—any channel member can forge a packet with any claimed UMSH source address or MeshCore sender name.
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; uses non-regressing UNIX timestamp values for advertisement freshness and login sequencing |
UMSH maintains a clean boundary between the MAC layer and higher-layer protocols. The MAC layer defines framing, addressing, encryption, authentication, and forwarding, and treats payload content as opaque. UMSH also defines its own application-layer protocols (text messaging, chat rooms, node identity, node management), but these are architecturally separate from the MAC layer and carried in the payload alongside any other higher-layer protocol.
MeshCore takes a more vertically integrated approach: the protocol directly defines payload types for text messages, node advertisements, login sequences, and multipart transfers without a clear separation between MAC and application concerns.
Timestamps and Time Dependency
MeshCore uses UNIX timestamp values in several protocol-critical roles:
- Replay protection: MeshCore uses a fixed-size circular buffer of packet hashes (160 entries) for short-term duplicate detection. Once the buffer wraps, previously seen packets can no longer be detected as duplicates. Application-layer timestamps provide additional protection for some message types, but there is no MAC-layer replay protection counter.
- Advertisement freshness: Node advertisements carry a timestamp. For an already-known contact, the receiver accepts an advertisement only if its timestamp is greater than the last accepted value from that contact.
- Login sequencing: Login handling compares the sender’s timestamp with the last accepted value from that sender.
UMSH is entirely timestamp-free at the MAC layer. Replay protection is based on monotonic 4-byte frame counters (see Frame Counter), which require no clock synchronization and no access to absolute time. Higher-layer payloads (such as the node identity payload in Node Identity) may optionally carry timestamps for application-level purposes, but the MAC layer neither requires nor interprets them.
UMSH’s monotonic frame counter provides cryptographic replay protection that does not depend on a clock and does not degrade as traffic volume increases. MeshCore’s hash-based duplicate cache provides short-term deduplication but has a fixed capacity—in a busy mesh, the 160-entry buffer can wrap quickly, allowing replayed packets to be accepted after the original entry is evicted. MeshCore’s advertisement and login checks require a sender’s timestamp values not to regress relative to previously accepted state. Clock reset or rollback can therefore cause valid traffic to be rejected, but these checks do not require nodes’ clocks to be accurate or synchronized with one another.
Packet Overhead Comparison
Minimum overhead for a typical encrypted unicast message (no options, no flood hop count):
| 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 a CTR IV derived from the transmitted MIC) and then computing S2V over the decrypted plaintext to confirm the MIC matches. Only a collision from a completely unknown sender transmitting with a full 32-byte source key (S=1) would additionally require ECDH and HKDF derivation. If the hint does not match, the packet can be discarded immediately with minimal CPU cost.
The problem is collisions. In a network of many nodes, some fraction of packets addressed to other nodes will collide with your own hint and trigger unnecessary cryptographic work. The collision rate depends on the hint width:
| 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 a packet sent to a different channel collides with one of the receiver’s configured channel identifiers and triggers an unnecessary verification attempt. UMSH’s 2-byte channel identifier reduces this to 1-in-65536.
Repeater Power
Both protocols use flood routing, so nodes configured as repeaters must receive and retransmit packets intended for other nodes. Transmit is the most power-intensive radio operation, so minimizing unnecessary retransmissions matters. In practice, repeating is enabled only on dedicated infrastructure nodes; end-user devices are typically configured as non-repeating nodes and incur no forwarding transmit cost.
For dedicated repeater nodes, UMSH does not require decrypting or verifying the payload MIC before forwarding—the MAC layer treats payloads opaquely, and forwarding decisions use routing metadata, duplicate suppression, and configured forwarding policy. In particular, region filtering consists of comparing the packet’s fixed two-byte region-code options with pre-derived local codes; it does not require an HMAC over the payload for every configured region. UMSH’s channel access mechanisms (CAD with random backoff, SNR-based contention windows) reduce collisions, and forwarding confirmation with retries provides reliable hop-by-hop delivery. The signal-quality filtering options (minimum RSSI and SNR) allow senders to prevent retransmission over weak links, avoiding wasted transmit power on paths unlikely to deliver the packet successfully.
MeshCore’s SNR-based retransmit delay implicitly prioritizes better-positioned repeaters—nodes with stronger reception retransmit sooner, which can suppress weaker nodes via duplicate detection. MeshCore does not drop a packet merely because its received signal quality is poor, but that does not mean every repeater forwards every flood. Forwarding remains subject to path and duplicate limits, configured region policy, and transport-code matching. For a transport-scoped packet, a repeater performs the payload HMAC described above for each candidate region key and drops the packet if none match. Eligible repeaters still lack UMSH’s explicit sender-selected signal-quality thresholds, which can prevent retransmission over weak links entirely.
What This Means in Practice
The protocol differences described above translate into concrete user-visible behaviors:
-
Multi-hop delivery is more reliable. UMSH defines hop-by-hop forwarding confirmation with retries, so each repeater along the path confirms receipt before the previous hop moves on. Versions of this are present for both source-routed paths and flooded packets in UMSH. MeshCore’s forwarding is fire-and-forget in both flood and direct modes—if a transmission is lost at any hop, there is no recovery mechanism at the forwarding layer, leaving application-level retries as the only recovery mechanism on MeshCore.
-
Replay protection does not depend on clocks. MeshCore uses non-regressing UNIX timestamp values for advertisement freshness and login sequencing, so a clock reset or rollback can cause valid advertisements or login attempts to be rejected until the timestamp advances beyond the previously accepted value. The clocks do not need to be accurate or synchronized. UMSH is timestamp-free at the MAC layer—its replay protection instead uses monotonic frame counters.
-
Fewer false wake-ups on busy networks. MeshCore’s 1-byte address hints mean that roughly 1 in 256 packets addressed to other nodes will appear to match yours, triggering unnecessary cryptographic verification before the packet can be discarded. UMSH’s 3-byte hints reduce this to roughly 1 in 16 million, which matters for battery-powered nodes that need to return to sleep quickly.
-
Routes can combine source routing and flooding. A UMSH packet can be source-routed to a known region and then flood locally from there, reaching a specific area without flooding the entire mesh. MeshCore treats flood and direct routing as mutually exclusive.
-
Amateur radio operation is a first-class concern. UMSH defines packet options for operator and station callsigns and an explicit unencrypted mode, allowing compliant operation on amateur radio frequencies where encryption is prohibited and station identification is required. MeshCore does not define equivalent mechanisms.
-
Easier to extend without breaking existing deployments. UMSH’s composable options and opaque payload model let developers add new routing behaviors, packet options, or application protocols without changing the MAC layer—existing nodes simply ignore options and payload types they do not recognize. MeshCore’s vertically integrated design, where application-layer semantics are defined at the protocol level, means that new features are more likely to require coordinated firmware updates across the network.
-
Stronger security from a coherent cryptographic design. MeshCore’s cryptographic choices—AES-128-ECB encryption, 2-byte MACs, a 160-entry duplicate cache for replay protection, and no key separation—individually and collectively weaken the security guarantees the protocol can offer. Identical messages produce identical ciphertext, MAC forgery is feasible to brute-force, replayed packets are accepted once the duplicate cache wraps, and the same key material is reused across cryptographic operations. UMSH addresses all of these with AES-CTR encryption using a synthetic IV, configurable MIC sizes up to 16 bytes, monotonic frame counters for replay protection, HKDF-derived domain-separated keys, and optional perfect forward secrecy sessions that protect past traffic if a long-term key is later compromised. Something like this cannot be easily bolted onto MeshCore’s current design.
Summary of Design Differences
MeshCore optimizes aggressively for minimal packet overhead in the common case, accepting significant cryptographic and flexibility tradeoffs to maximize payload capacity within LoRa frame constraints. It takes a vertically integrated approach, defining application-layer payload types and relying on a fixed-size duplicate cache for deduplication and UNIX timestamps for advertisement freshness.
UMSH prioritizes cryptographic robustness, composable routing, privacy features, and strict layer separation, accepting higher overhead in exchange for stronger security guarantees and a more extensible protocol structure. By restricting itself to the MAC layer and using monotonic frame counters instead of timestamps, UMSH avoids coupling to specific application assumptions or clock synchronization requirements. The S flag allows the overhead to scale based on whether the receiver already knows the sender’s public key, bridging the gap for established communication pairs while still supporting zero-prior-state first contact.
Comparison with Meshtastic
This section compares UMSH with Meshtastic, a popular open-source LoRa mesh project. The comparison is based on the exact Meshtastic firmware tag v2.7.26.54e0d8d and its pinned protobuf definitions.
Note
This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!
Meshtastic and UMSH occupy different positions in the design space. Meshtastic is a mature, widely deployed application-focused system optimized for ease of use and broad hardware support. UMSH prioritizes authenticated communication, compact encoding, and explicit separation between its MAC and application protocols. The comparison below describes technical differences without implying that one set of tradeoffs is universally better than the other.
Identity and Addressing
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Identity basis | 32-byte Ed25519 public key | 32-bit node number, normally initialized from the low 32 bits of a platform hardware identifier or MAC-like address; may be randomly reassigned after a detected collision |
| Cryptographic identity | Public key is the address | Curve25519 keypair generated and used for PKC where supported; 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 or broadcast value (cleartext) |
| Channel identifier | 2-byte derived hint | 1-byte XOR of channel-name bytes and effective key bytes |
| Address spoofing resistance | Cryptographic for pairwise unicast because keys are derived from public-key addresses | None for channel traffic; PKC authenticates against a stored or verified public key, but the node number is not cryptographically bound to that key |
UMSH identifies nodes by their Ed25519 public keys, which serve as both the address and the cryptographic credential. Possession of the corresponding private key is required to authenticate and decrypt pairwise traffic. Meshtastic’s normal initial node number comes from a platform hardware identifier, but this is not universally a Bluetooth MAC and collision handling can assign a random replacement. The node number itself is not cryptographically bound to a key.
On supported, non-amateur builds, Meshtastic normally generates a Curve25519 keypair and uses PKC automatically for eligible unicast traffic when the peer key is known. A learned public key is associated with a node number and later mismatches are rejected; users can also verify keys explicitly. This provides conditional sender authentication for PKC traffic, but it remains a stored or verified association rather than an address derived from the key. Channel-encrypted traffic has no per-node authentication regardless of whether PKC keys are configured.
Packet Structure
| 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+ registered values) |
| 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 (encoded maximum 7) |
| Max LoRa payload | Approximately 255 bytes | 255-byte frame; the Data.payload schema permits 233 bytes, but usable application capacity depends on the encoded Data fields and security mode |
| Common unicast overhead relative to application body | 17 bytes with compact source and 4-byte MIC, or 29 bytes with 16-byte MIC; excludes salt, FHOPS, and options | Approximately 22–23 bytes for channel encryption or 34–35 bytes for PKC, depending on protobuf length encoding |
Before counting UMSH’s 1-byte payload-type prefix, FHOPS, or packet options, the fixed fields of a compact-source secured unicast occupy 16–30 bytes depending on MIC length and salt presence. A full 32-byte source raises that range to 45–59 bytes. The common-case row above adds the payload-type prefix and selects no salt with either a 4-byte or 16-byte MIC.
UMSH uses a compact 1-byte Frame Control Field with optional expansion: fields are present only when needed for the packet type. Meshtastic uses a fixed 16-byte header on every packet, with source and destination node numbers, packet ID, flags, channel hash, and routing fields always present.
Meshtastic’s header is always transmitted in cleartext, exposing sender node number, destination field, packet ID, and channel hash to any passive observer. For unicast, the destination field identifies the recipient; for broadcast, it contains the broadcast value rather than an individual recipient. UMSH’s addressing fields are compact hints that do not directly reveal a complete node identity, and blind unicast and encrypted multicast conceal their address fields as defined below.
Meshtastic encodes application payloads in the schema-based, extensible protobuf Data envelope. UMSH uses raw byte payloads with a 1-byte type prefix, minimizing encoding overhead while leaving structure to the selected application protocol.
Cryptography
| Aspect | UMSH | Meshtastic (channel) | Meshtastic (eligible unicast PKC) |
|---|---|---|---|
| Encryption | AES-256-CTR (AES-SIV, RFC 5297) | AES-128-CTR or AES-256-CTR, selected by effective PSK length; the default effective key is 16 bytes | AES-CCM |
| Authentication | S2V (AES-CMAC), 4/8/12/16-byte MIC | None | 8-byte CCM authentication tag |
| Key exchange | X25519 ECDH | Pre-shared key | Curve25519 ECDH |
| Key derivation | HKDF-SHA256 with domain separation | Effective PSK used directly | SHA-256 of ECDH shared secret |
| Nonce construction | Frame counter + optional salt in SECINFO | Packet ID + sender node number | Packet ID + 4-byte random contribution + sender node number |
| Duplicate/replay handling | 4-byte monotonic frame counter with replay window | Partially randomized 32-bit packet ID and finite duplicate cache | Same packet-ID duplicate cache as channel traffic |
| 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:
- Ciphertext can be modified in transit without a cryptographic integrity check. When an attacker knows or can predict the corresponding plaintext, CTR bit flipping can be targeted without knowing the key.
- Any node with the channel key can forge packets claiming to be from another node, since channel mode has no per-node authentication and the sender’s node number in the cleartext header is not cryptographically bound to the ciphertext.
UMSH authenticates every secured packet with an S2V MIC (4–16 bytes; see MIC Size Selection Guidance). Even with a 4-byte MIC, UMSH provides 2^-32 forgery resistance, which is qualitatively different from the absence of an integrity check on Meshtastic channel traffic. A UMSH channel packet cannot be modified in transit or injected by a non-member without detection.
The second bullet above, however, describes a property UMSH multicast shares, and it is not a difference between the two protocols. A shared symmetric key proves channel membership but cannot distinguish one member from another, so a UMSH channel member can likewise claim another member’s source address; see Multicast Sender Authentication. What 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 for Eligible Unicast Traffic
Meshtastic uses Curve25519 ECDH with AES-CCM for eligible unicast traffic, providing confidentiality and authentication against the public key associated with the claimed sender node number. It is not limited to text direct messages. The firmware considers PKC automatically for locally originated, non-broadcast application traffic when keys are available, except for position, node info, routing, traceroute, amateur operation, and certain serial/GPIO cases. Broadcast traffic such as position, telemetry, and channel text remains in channel mode and therefore unauthenticated.
UMSH applies the same S2V-based construction to secured unicast and multicast, so there is no secured traffic class left unauthenticated and no separate authenticated mode to opt into. Broadcast packets carry no security information and make no claim of authenticity.
Key Derivation
Meshtastic uses the effective channel PSK directly as the AES key for channel encryption. A configured one-byte PSK is not a one-byte AES key: it is a public shorthand alias expanded by the firmware into an effective 16-byte key. For PKC, the ECDH shared secret is hashed with SHA-256 to produce the AES key.
UMSH uses HKDF-SHA256 with domain-separated labels to derive independent encryption and authentication keys from each shared secret. This prevents cross-purpose key reuse and follows standard cryptographic practice.
Routing
| 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 |
| Source route followed by flood tail | Yes, both can be carried in the same packet | No; learned next-hop delivery can fall back to flooding, but there is no source-route field |
| Next-hop routing | N/A | Yes (learned from ACK paths, v2.6+) |
| Hop budget | 15 flood hops plus source-routed hops | 3-bit encoded budget (max 7); selected favorite-router infrastructure hops may preserve the budget |
| Duplicate detection | MIC cache | Finite cache keyed by sender and partially randomized 32-bit packet ID |
| Forwarding confirmation | Hop-by-hop retries with backoff for source-routed hops; retries at a flood origin | Reliable (want_ack) origin: initial transmission plus at most two retransmissions; directed intermediate: initial transmission plus at most one retransmission; intermediate flood relays do not retry |
| Channel access | CAD with random backoff; SNR-based contention windows | CAD with random backoff; channel-utilization-based local contention and SNR-based flood contention |
| Signal-quality controls | Min RSSI and min SNR eligibility options | Lower-SNR/farther flood candidates tend to transmit earlier; infrastructure roles also affect priority |
| 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 discovery and fallback mechanism. Meshtastic’s managed flood gives lower-SNR receptions smaller contention windows, treating apparent distance from the sender as a heuristic for useful forwarding progress. ROUTER nodes receive a separate early-transmission advantage. Most roles cancel a queued rebroadcast after overhearing a duplicate, while infrastructure roles such as ROUTER may intentionally transmit anyway. UMSH instead provides explicit signal-quality thresholds that allow the sender to control flood-relay eligibility per packet.
Meshtastic’s 3-bit field encodes a maximum hop budget of 7, but this is not an unconditional cap on the physical number of relays. After the first hop, selected transitions between configured ROUTER, ROUTER_LATE, or CLIENT_BASE infrastructure nodes can preserve the budget when the previous relay is a favorite. UMSH’s 4-bit flood hop count permits up to 15 flood hops, while source-routed hops consume packet space rather than flood budget.
Meshtastic v2.6+ added next-hop routing for unicast: after a successful ACK exchange, the firmware learns which relay carried the response and uses that relay as the designated next hop for subsequent packets. UMSH achieves directed delivery through source-route options learned via trace routes. The recipient can cache the accumulated trace directly as a source route for subsequent communication with the sender because the trace is already built most-recent hop first; see Route Learning.
Both protocols perform channel activity detection and randomized contention. Meshtastic sizes normal local-send contention from recent channel utilization and uses received SNR for flood-rebroadcast contention. UMSH uses CAD with random backoff and SNR-based contention windows as defined by its channel-access rules.
Both protocols provide forwarding confirmation, but the scopes differ. For a Meshtastic want_ack transmission, the origin makes at most three total attempts: the initial transmission and two retransmissions. When a learned next hop is in use, the final origin attempt falls back to flooding. A directed intermediate relay also listens for its designated next hop and may retransmit once; intermediate flood relays do not retry because no single next hop is designated. UMSH applies passive forwarding confirmation and retry to each source-routed hop and to a flood origin, while intermediate flood relays likewise do not retry.
Privacy
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Header confidentiality | Addressing fields are compact hints; blind modes encrypt source/destination | Header always cleartext: sender, destination field, packet ID, and channel hash exposed |
| Source concealment | Encrypted multicast, blind unicast | Not supported |
| Destination concealment | Blind unicast | Not supported |
| Node ID linkability | Public key; ephemeral keys are supported | Normally persistent hardware-derived node number, with possible random collision reassignment |
| Anonymous first contact | Ephemeral Ed25519 key with S=1 flag | Not supported |
Meshtastic’s 16-byte cleartext header exposes the complete sender node number and destination field on every packet. A unicast packet therefore exposes both node numbers; a broadcast exposes the sender and the broadcast destination value. A passive observer can still correlate sender node numbers, build traffic graphs, and track a normally persistent identifier over time without the channel key.
UMSH’s compact hints reveal less information directly, and blind unicast and encrypted multicast modes conceal their defined source and/or destination fields. Nodes can also use ephemeral keypairs for anonymous communication.
Multicast and Group Communication
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Channel key size | 32 bytes | Configured as 0, 1-byte public alias, 16, or 32 bytes; effective AES key is 0, 16, or 32 bytes |
| Channel identifier | 2-byte derived hint | 1-byte XOR of channel-name bytes and effective key bytes |
| 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 S2V MIC | 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 a node to 8 configured channels. Under an independent uniform-candidate model, a specific unrelated channel has a 1-in-256 chance of matching Meshtastic’s 1-byte hint and a 1-in-65536 chance of matching UMSH’s 2-byte identifier. With m independent configured candidates, the probability of at least one match is 1-(255/256)^m for Meshtastic and 1-(65535/65536)^m for UMSH. Meshtastic’s XOR construction is not a cryptographic hash, so these are modeling assumptions rather than exact operational rates. A matching Meshtastic hint causes the receiver to try the corresponding channel key and validate the resulting protobuf.
Application Layer
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Payload typing | 1-byte payload type prefix | Protobuf portnum field (30+ registered values) |
| Payload encoding | Raw bytes | Protocol Buffers or port-specific binary/text formats |
| 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; Store-and-Forward++ schema for native Linux nodes |
| Other registered integrations | Higher-layer protocols can be assigned payload types | TAK/ATAK (including V2), Reticulum, LoRaWAN bridge, Cayenne, and remote-shell wire schema, among others |
| Amateur radio | Operator/station callsign options, explicit unencrypted mode | is_licensed state plus dedicated ham-mode command; callsign in long name, automatic PSK/admin removal, and licensed relay restrictions |
| Implementation | Protocol spec (language-agnostic) | C++ firmware + protobuf definitions and companion applications |
Meshtastic defines a rich application ecosystem with built-in firmware support, companion-side integrations, and registered wire schemas for position sharing, telemetry, waypoints, audio, store-and-forward, TAK, Reticulum, remote shell, and other uses. Registration of a PortNum and schema does not imply that every firmware build contains a native handler for that application.
UMSH defines a smaller set of application protocols (text messaging, chat rooms, node identity, node management) and delegates richer application functionality to higher-layer protocols carried in the payload, such as CoAP. This is less feature-complete out of the box but allows UMSH to carry arbitrary higher-layer content without changing the MAC protocol.
Both protocols address amateur radio operation, but at different levels. Meshtastic’s dedicated ham-mode command sets the callsign as the node’s long name, sets the short name and radio overrides, raises the NodeInfo cadence required for identification, changes rebroadcasting to local-only, and automatically removes channel PSKs and admin access. Setting the raw is_licensed owner flag also clears encryption automatically, but the operator must separately ensure the callsign fields are populated. Licensed nodes refuse to relay packets to or from nodes known to be unlicensed. The callsign is carried in the existing user-info field rather than a dedicated per-packet protocol field.
UMSH defines dedicated packet options for operator and station callsigns, and its security-control field explicitly indicates whether encryption is enabled. In licensed-only mode, monitoring software can verify the presence of the required callsign option and the absence of encryption directly from the packet. Both protocols still require the operator to select and configure the lawful operating mode.
Layer Separation
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Protocol organization | MAC layer with separately specified application protocols | Shared firmware and protobuf registry spanning radio integration, routing control, and applications |
| Forwarding-time payload interpretation | Opaque at the MAC layer | Clear raw header is sufficient for forwarding; encrypted Data may remain opaque to a relay |
| Fragmentation | Delegated to higher-layer protocols | No generic mesh-layer fragmentation; individual applications or transports may define their own |
| Application registration | Application protocols are architecturally separate | 30+ application identifiers registered in the core PortNum schema |
UMSH specifies an explicit boundary between its MAC layer and application protocols: the MAC treats payloads opaquely and can carry any assigned higher-layer protocol. Meshtastic likewise has a concrete wire boundary between its 16-byte raw forwarding header and the encrypted protobuf Data envelope, so a relay can forward traffic it cannot decrypt. Its Data schema also contains port, payload, response-correlation, and some routing-related fields, and its application identifiers live in the shared core protobuf registry. The result is greater integration between routing, firmware modules, and application dispatch than in UMSH, but not an absence of all layer boundaries.
Neither protocol defines generic fragmentation at the mesh/MAC layer. UMSH explicitly delegates it to higher-layer protocols such as CoAP block-wise transfer. Meshtastic applications and companion transports can define their own mechanisms; for example, the Reticulum tunnel port is specified to carry fragmented RNS packets.
Timestamps and Time Dependency
| Aspect | UMSH | Meshtastic |
|---|---|---|
| Replay/duplicate handling | 4-byte monotonic frame counter with replay window | Partially randomized 32-bit packet ID and finite duplicate cache |
| Timestamps in packets | None at MAC layer | rx_time metadata (not transmitted in the LoRa header and not used for routing decisions) |
| Clock synchronization required | No | No |
Neither protocol requires clock synchronization for core operation. Meshtastic records reception timestamps as local metadata but does not use them for routing or duplicate suppression. UMSH provides replay protection with monotonic frame counters and a receive window. Meshtastic suppresses recent duplicates by caching the sender and partially randomized packet ID; because that cache is finite, this should not be described as durable cryptographic replay protection.
Packet Overhead Comparison
The following compares overhead relative to the application body for a common secured unicast message. The UMSH columns use a compact source, no salt, no FHOPS, and no packet options. The Meshtastic columns assume a common one-byte portnum value and the locally originated Data.bitfield; the protobuf envelope is 6 bytes while the application body is shorter than 128 bytes and normally 7 bytes once its length requires a two-byte varint.
| Field | UMSH (S=0, 16B MIC) | UMSH (S=0, 4B MIC) | Meshtastic (channel) | Meshtastic (PKC) |
|---|---|---|---|---|
| Header/FCF | 1 | 1 | 16 | 16 |
| Destination | 3 | 3 | (in header) | (in header) |
| Source | 3 | 3 | (in header) | (in header) |
| SECINFO | 5 | 5 | — | — |
| Payload-type prefix | 1 | 1 | — | — |
| MIC | 16 | 4 | — | — |
Protobuf Data envelope | — | — | 6–7 | 6–7 |
| PKC tag + random nonce contribution | — | — | — | 12 |
| Total overhead | 29 | 17 | 22–23 | 34–35 |
Meshtastic channel mode derives its AES-CTR nonce from the cleartext packet ID and sender node number and transmits no separate channel nonce. PKC uses those header fields plus a transmitted 4-byte random contribution and carries an 8-byte CCM tag, adding exactly 12 bytes beyond an otherwise equivalent channel Data envelope.
UMSH with a 16-byte MIC has 29 bytes of application-body-to-wire overhead under the stated conditions, compared with approximately 22–23 bytes for Meshtastic channel mode. UMSH’s total includes authentication that Meshtastic channel mode lacks. With a 4-byte MIC, UMSH uses 17 bytes under the same conditions while still providing a truncated authentication tag.
The Meshtastic Data.payload field is sized for as many as 233 bytes, but that schema capacity is not a universal application-body limit. For a common locally originated text packet, the encoded port, payload length, and required bitfield make about 232 bytes fit in channel mode; the additional 12 PKC bytes reduce the analogous PKC capacity to about 220 bytes. Other Data fields or larger portnum encodings reduce it further.
Power Consumption
Power consumption on a battery-constrained LoRa node is influenced by radio airtime, receive duty cycle, retransmission behavior, and CPU work after a packet is received. The protocol and firmware establish which operations occur, but their practical energy magnitude requires measurement on representative hardware.
Channel Filtering and False Positives
For channel traffic, a compact channel identifier is a pre-crypto filter. If an unrelated incoming channel produces the same identifier as one configured locally, the receiver must try the local candidate key to determine whether the packet is valid.
| Protocol | Channel identifier width | Uniform per-candidate match probability |
|---|---|---|
| Meshtastic | 8 bits (XOR of channel name and effective key) | 1 in 256 |
| UMSH | 16 bits (derived hint) | 1 in 65536 |
Under independent uniform assumptions, the per-candidate probability differs by a factor of 256. With m configured candidates, the probability of at least one match is 1-(255/256)^m for Meshtastic and 1-(65535/65536)^m for UMSH. Meshtastic’s XOR construction is not a uniform cryptographic hash in every real configuration, so these figures are a model rather than measured traffic rates.
The Meshtastic receive loop confirms the mechanism: every configured channel whose hint matches triggers an AES-CTR decryption and protobuf parse attempt until one succeeds or the candidates are exhausted. This is additional CPU work, but no measurement cited here establishes a meaningful battery-life impact, and the AES cost may be small compared with LoRa receive airtime.
Unicast Filtering
For unicast packets, Meshtastic’s 4-byte cleartext node number provides an exact destination comparison before payload decryption. UMSH’s 3-byte destination hint has a modeled 1-in-16,777,216 per-identity false-positive probability. A collision requires cryptographic processing to determine whether the packet is actually addressed to the receiver; pairwise keys can be cached after first contact, so known-sender handling does not require a fresh ECDH operation.
This is a protocol tradeoff: Meshtastic spends one additional cleartext destination byte and exposes the complete unicast node number, while UMSH uses a shorter, less identifying prefilter with a small collision probability. The practical power significance of that difference depends on traffic and hardware and is not asserted here.
Packet Length and Airtime
Meshtastic’s fixed 16-byte header is transmitted on every packet. UMSH includes only fields required by the packet type. Longer LoRa packets consume more airtime and receiver-on time, but the total comparison depends on UMSH’s selected MIC, optional fields, and source form as well as on Meshtastic’s security mode and protobuf envelope.
Forwarding Power
Both protocols use flooding, so forwarding nodes may receive and retransmit packets. Transmit is normally among the most power-intensive radio operations. Meshtastic defaults to the CLIENT role with rebroadcast mode ALL, so ordinary client nodes participate in managed rebroadcasting. A node that should not forward must use CLIENT_MUTE, rebroadcast mode NONE, or another appropriately restrictive configuration. The old REPEATER role is deprecated; current infrastructure configurations include roles such as ROUTER, ROUTER_LATE, and CLIENT_BASE.
Meshtastic’s managed flood gives lower-SNR receptions shorter expected delays, allowing nodes that appear farther from the sender to relay earlier. Most client-like roles cancel a queued transmission after overhearing a duplicate. ROUTER nodes receive separate early priority and intentionally do not cancel every duplicate. These policies reduce or redirect redundant transmissions compared with an unsuppressed flood, but the effect depends on topology and role configuration.
UMSH’s minimum-RSSI and minimum-SNR options allow the original sender to set explicit flood-forwarding thresholds. A repeater below the threshold does not retransmit the packet. Meshtastic’s approach chooses contention priority automatically; UMSH’s approach makes relay eligibility explicit but requires the sender to choose suitable thresholds.
UMSH repeaters do not need to decrypt or verify the application payload before forwarding because the MAC layer treats it opaquely. Meshtastic forwarding likewise uses the clear raw header and can relay both channel and PKC ciphertext without decrypting the protobuf Data body.
Summary of Design Differences
Meshtastic is a full-featured mesh communication system with a large and active user community. It provides a rich application ecosystem, broad hardware support, and an easy on-ramp for non-technical users. Its channel-based encryption model is simple to configure and deploy, while eligible unicast traffic can use PKC when peer keys are available.
UMSH prioritizes authenticated secured traffic, compact encoding, metadata-concealment modes, and explicit MAC/application boundaries. Secured unicast and multicast packets carry authentication; unsecured broadcast packets intentionally make no authenticity claim.
Key tradeoffs:
- Authentication: UMSH authenticates every secured unicast and multicast packet. Meshtastic channel traffic has no integrity check; eligible PKC unicast traffic is authenticated against the stored or verified peer key.
- Privacy: UMSH provides compact addressing hints and opt-in blind modes. Meshtastic exposes the complete sender and destination fields in every cleartext header; unicast therefore exposes both endpoint node numbers, while broadcast uses the broadcast destination value.
- Identity model: UMSH uses cryptographic public keys as addresses. Meshtastic uses a normally hardware-derived node number that is not cryptographically bound to its PKC key.
- Overhead: Under the conditions in the overhead table, UMSH uses 17 bytes with a 4-byte MIC or 29 with a 16-byte MIC; Meshtastic uses approximately 22–23 bytes for channel mode or 34–35 for PKC. UMSH’s totals include authentication, while Meshtastic channel mode does not.
- Application richness: Meshtastic provides a much richer built-in and companion application ecosystem. UMSH delegates richer functionality to higher-layer protocols.
- Protocol organization: UMSH specifies MAC and application protocols separately. Meshtastic has a distinct raw forwarding header and encrypted application envelope, while application registration, routing control, and firmware modules share a common protobuf and firmware ecosystem.
- Implementation: Meshtastic is a mature C++ firmware with broad device support. UMSH is not tied to an implementation language or runtime, and its compact wire design can target bare-metal microcontrollers.
- Ease of deployment: Meshtastic is designed for immediate use with consumer hardware. UMSH requires implementation effort and explicit key configuration.
Comparison with Reticulum
This section compares UMSH with Reticulum, a cryptography-based networking stack designed for operation over a wide range of mediums, including LoRa. The comparison is based on Reticulum v1.1.4 (tag 1.1.4) and its manual and source code.
Note
This comparison aims to be as fair and accurate as possible, not promotional material. If you spot any unfair comparisons, factual errors, or other mistakes, please file an issue!
The Reticulum claims in this document can be independently verified against the following source files:
| 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-256-CTR (AES-SIV, RFC 5297) | AES-256-CBC with PKCS7 padding (Token.py:91); AES-128 support removed in v1.0.0 |
| Authentication | S2V (AES-CMAC), 4/8/12/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 (CTR IV derived from the MIC) | 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 AES-SIV (RFC 5297), in which the synthetic IV serves as both the MIC and the source of the CTR IV, providing nonce-misuse resistance. Reticulum uses AES-256-CBC with a random 16-byte IV. Both are sound constructions—the primary difference in practice is overhead: CBC requires transmitting a 16-byte IV and adds 1–16 bytes of PKCS7 padding, while UMSH’s SIV construction derives the IV from the MIC (which is already transmitted) and uses CTR mode which requires no padding.
Authentication and Integrity
UMSH’s S2V MIC is configurable at 4, 8, 12, or 16 bytes (see MIC Size Selection Guidance), providing 32-bit to 128-bit integrity protection. Reticulum’s 32-byte HMAC-SHA256 tag provides 256-bit integrity. Both are sound choices; UMSH’s configurable size allows deployments to trade integrity margin for payload capacity within the constrained LoRa frame budget.
Key Management
For unicast, UMSH derives stable pairwise keys from the ECDH shared secret via HKDF with domain-separated labels. These keys are reused across packets, with per-packet variability provided by the frame counter and optional salt in SECINFO. This is efficient: no per-packet key exchange overhead. For forward secrecy, UMSH defines PFS sessions in which both nodes exchange ephemeral node addresses via a two-message handshake and communicate using session-specific keys for an agreed duration. Compromise of long-term keys does not expose traffic encrypted under PFS session keys. PFS sessions add no per-packet overhead once established.
Reticulum uses two approaches. For SINGLE (one-off) destinations, each packet includes a fresh ephemeral X25519 public key (32 bytes), providing per-packet forward secrecy at substantial overhead cost—32 extra bytes on every packet (Identity.py:574, ephemeral_key = X25519PrivateKey.generate()). For LINK (session) destinations, a single ECDH exchange establishes symmetric keys that persist for the link’s lifetime—similar to UMSH’s stable pairwise keys (Link.py:340, self.shared_key = self.prv.exchange(self.peer_pub)). Reticulum also offers a ratchet mechanism that rotates keys at a configurable minimum interval (default 30 minutes, adjustable per-destination via Destination.set_ratchet_interval() (Destination.py:514)), providing periodic forward secrecy within a link (Destination.py:90, RATCHET_INTERVAL = 30*60). Up to 512 ratchet keys are stored per destination, each expiring after 30 days. Ratchet key presence is signalled via a context flag in announce packets, allowing senders to use the most recent ratchet key in place of the static identity key, providing forward secrecy for single-packet communication without the full 32-byte ephemeral pubkey overhead.
Both protocols offer forward secrecy, but with different granularity and overhead tradeoffs. Reticulum’s SINGLE mode provides per-packet forward secrecy at 32 bytes per packet; UMSH’s PFS sessions provide per-session forward secrecy at zero per-packet overhead after setup.
Replay Protection
UMSH uses explicit 4-byte monotonic frame counters, which provide deterministic, stateless replay detection with a well-defined forward window. A receiver can immediately reject a replayed packet by comparing the counter to its stored state.
Reticulum detects duplicates by caching packet hashes (Transport.py:59, packet_hashlist = set()). This approach works but has different tradeoffs: it requires maintaining a hash cache, and once the cache fills it is evicted in bulk via a two-generation rolling scheme—when the active set exceeds 500,000 entries it is moved into a packet_hashlist_prev set (Transport.py:60) and a fresh set starts accumulating (Transport.py:565–567, cap: Transport.py:115, hashlist_maxsize = 1000000). A replayed packet that was seen in neither the current nor the previous generation would not be detected. The reference implementation’s 500,000-entry threshold assumes ample memory; on constrained hardware where the cache must be significantly smaller, the window for undetected replay narrows accordingly.
Cryptographic Overhead
For an encrypted unicast message, the total cryptographic overhead differs significantly:
| 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 S2V (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 a CTR IV derived from the transmitted MIC) and then computing S2V over the decrypted plaintext to confirm the MIC matches. ECDH and HKDF would additionally be required for a false positive from an unknown sender transmitting with a full 32-byte source key (S=1), which is rare in normal operation. In practice, on a LoRa network with modest traffic, spurious collisions are rare enough that this cost is negligible.
Packet Length and Airtime
Reticulum’s total per-packet overhead is roughly 91 bytes (LINK) or 108 bytes (SINGLE), combining header and cryptographic fields. UMSH’s total overhead is 14–28 bytes for a typical authenticated unicast. Shorter packets mean less airtime per message, which translates directly to less receive power for every node in range—a cost the sender imposes on the whole network.
Fragmentation
Reticulum’s 500-byte network MTU exceeds the ~255-byte LoRa frame limit, requiring link-layer fragmentation for larger messages. A node receiving a fragmented message must keep its radio and MCU active across multiple frames until reassembly completes. UMSH is designed to fit each packet into a single LoRa frame, so the radio can return to sleep as soon as one frame is processed.
Announce Traffic and Repeater Power
Reticulum relies on periodic announce flooding to build and maintain path tables. Transport Nodes re-broadcast announces on all interfaces (subject to a default 2% bandwidth cap); regular nodes receive announces but do not re-broadcast them. This creates a continuous background of traffic that all nodes must receive and that Transport Nodes must retransmit. UMSH does not define an equivalent mechanism—path discovery is on-demand via the trace-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") | AE 71 DC 38 72 |
| MIC | 16 bytes | 61 8E 96 38 FE 4D 9A E8 34 33 1D E8 E0 DD 06 3E |
D0 6C 28 FD ED 54 A5 E0 00 00 00 2A FF AE 71 DC
38 72 61 8E 96 38 FE 4D 9A E8 34 33 1D E8 E0 DD
06 3E
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") | F8 82 EE |
| MIC | 16 bytes | AA 17 13 06 26 1C E7 FF F2 FF 01 7F 90 10 A7 D9 |
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 F8 82 EE AA 17 13
06 26 1C E7 FF F2 FF 01 7F 90 10 A7 D9
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 | AC BF 20 01 42 05 B1 04 17 5E A6 8F 66 47 78 83 |
E0 B0 8D E0 00 00 00 05 FF 7C 16 CC CF 27 32 48
78 AC BF 20 01 42 05 B1 04 17 5E A6 8F 66 47 78
83
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 | 9A 4B FC DE 39 42 FE B2 25 B8 D3 D4 BC E7 9F DB |
E0 B0 8D 60 00 00 00 03 FF ED 54 A5 03 48 65 6C
6C 6F 9A 4B FC DE 39 42 FE B2 25 B8 D3 D4 BC E7
9F DB
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") | 81 2D 2F |
| MIC | 16 bytes | BA 19 2E EA B5 7D 71 E3 52 BD 7D DF 33 1B 07 27 |
D1 40 6C 28 FD ED 54 A5 E0 00 00 00 0A 20 92 78
53 FF 81 2D 2F BA 19 2E EA B5 7D 71 E3 52 BD 7D
DF 33 1B 07 27
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") | 88 94 03 C3 07 |
| MIC | 16 bytes | C7 46 F3 5E 82 28 3E 3C 14 B0 5D 97 56 7B 4E 86 |
F0 B0 8D E0 00 00 00 07 FF D5 EC 8B 3D 69 96 88
94 03 C3 07 C7 46 F3 5E 82 28 3E 3C 14 B0 5D 97
56 7B 4E 86
Total: 36 bytes.