Mesh Protocol
A technical overview of how Socialmesh extends the Meshtastic radio protocol to support presence beacons, compact signals, and node identity exchange — without modifying firmware.
Contents
1. Overview
Socialmesh is a companion app for Meshtastic mesh radios. It adds features that the stock Meshtastic app does not provide: presence awareness, broadcast signals with expiry, and a node identity system called NodeDex.
These features need to send data over the mesh. Rather than forking Meshtastic firmware, Socialmesh uses private portnums — a range (256–511) that Meshtastic reserves for third-party applications. Stock firmware routes packets on private portnums identically to any other packet. No firmware changes are required.
Socialmesh defines three new packet types, each on its own portnum:
| Portnum | Name | Purpose |
|---|---|---|
260 |
SM_PRESENCE | "I am here" beacon |
261 |
SM_SIGNAL | Broadcast signal (alert, notice) |
262 |
SM_IDENTITY | Node identity digest (sigil, trait) |
A fourth portnum, 256, carries legacy JSON-encoded signals for backward
compatibility with older Socialmesh releases.
All three new packet types use hand-packed binary encoding rather than protobuf. The result is smaller payloads, less airtime, and more room on the mesh for everyone.
2. Why Binary?
LoRa is a slow, shared radio channel. Every byte costs airtime, and airtime is the scarcest resource on a mesh network. The original Socialmesh signal format used JSON on portnum 256. A typical signal with a short message and GPS coordinates consumed roughly 130 bytes.
The binary encoding reduces this to about 50 bytes — a 60% reduction. Presence beacons, which previously had to piggyback on full signal payloads, shrink to as little as 3 bytes.
| Use Case | JSON (Legacy) | Binary | Reduction |
|---|---|---|---|
| Signal with GPS + short text | ~130 bytes | ~50 bytes | ~60% |
| Signal without GPS | ~80 bytes | ~30 bytes | ~62% |
| Presence beacon with GPS | ~130 bytes | ~12 bytes | ~91% |
| Node identity exchange | N/A | ~9 bytes | (new) |
The encoding is also deterministic. Every field has a fixed size and position (conditionally present based on flags), which makes byte budgets easy to enforce and decode logic trivial.
3. Packet Families
SM_PRESENCE
An ephemeral "I am here" beacon. A node broadcasts its presence periodically to let nearby nodes know it is active. A presence packet can optionally include:
- GPS coordinates (latitude, longitude)
- Battery level (0–100%)
- Intent (idle, monitoring, traveling, available, busy, emergency, relay)
- Short status string (up to 63 bytes of UTF-8 text)
Presence is local by default. Packets use a low hop limit (2 hops) and background priority, so they stay within the immediate neighborhood and yield to higher-priority traffic under congestion.
There is no mesh-enforced TTL. The receiving app applies confidence decay: a node is "active" for 2 minutes after a beacon, "fading" until 10 minutes, and "stale" after 60 minutes.
Typical size: 3–75 bytes depending on optional fields.
SM_SIGNAL
A broadcast signal — a short, expiring message intended for all nodes in range. Signals can carry:
- Text content (up to 140 bytes of UTF-8)
- GPS coordinates (optional)
- TTL class — a client-side expiry hint (15 minutes to 24 hours)
- Priority (normal, important, urgent, emergency)
- Image flag — indicates an image is available through a separate channel
Each signal carries an 8-byte random ID used for deduplication and cloud sync mapping.
| Priority | Hop Limit | Acknowledgment |
|---|---|---|
| Normal | 3 | No |
| Important | 3 | No |
| Urgent | 3 | Yes |
| Emergency | 5 | Yes |
TTL is strictly a client-side display hint. The mesh has no mechanism to enforce expiry or recall a broadcast. Receiving apps use the TTL class to decide when to stop showing the signal.
Typical size: 21–159 bytes depending on content length and optional fields.
SM_IDENTITY
A compact identity digest used by the NodeDex system. Nodes exchange:
- Sigil hash — a deterministic 32-bit hash of the node number, used to verify that the sender is who they claim to be
- Trait — a behavioral classification (wanderer, beacon, ghost, sentinel, relay, courier, anchor, drifter)
- Encounter count — how many unique nodes this node has seen (self-reported)
Identity supports a request/response protocol:
- Request: Node A asks Node B for its identity (unicast, 6 bytes)
- Response: Node B replies with its full digest (unicast, 6–9 bytes)
- Unsolicited broadcast: A node announces itself on first appearance or trait change
Unsolicited identity broadcasts use a hop limit of 1 (direct neighbors only). Requests and responses use a hop limit of 3 to reach specific nodes.
Typical size: 6–9 bytes.
4. The Header Byte
All three packet types share a common first byte, called hdr0:
hdr0 = (version << 4) | kind
- High nibble (4 bits): Protocol version (currently 0)
- Low nibble (4 bits): Packet kind (1 = presence, 2 = signal, 3 = identity)
This gives room for 16 protocol versions and 16 packet kinds with zero additional overhead — a single byte.
The second byte is always a flags byte. Its layout is specific to each packet kind, controlling which optional fields are present. Together, the two-byte header tells the decoder everything it needs to parse the rest of the payload.
Version tolerance: If a receiver encounters a version higher than it supports, it silently discards the packet. Unknown kind values are also silently discarded. This allows future protocol revisions to coexist on the same mesh without breaking existing implementations.
5. Signal IDs & Collision Probability
Each SM_SIGNAL carries an 8-byte (64-bit) random identifier generated by a cryptographically secure random number generator. This ID serves two purposes:
- Deduplication. The mesh naturally rebroadcasts packets. The signal ID lets receivers discard duplicates they have already processed.
- Cloud sync mapping. For users who optionally sync signals to the cloud, the app maps the 64-bit wire ID to a local UUID.
The ID is not a secret — it travels in the clear alongside the signal content. Its purpose is uniqueness, not secrecy. A 64-bit random ID saves 28 bytes per signal compared to a string UUID, which matters when every byte costs airtime.
Collision probability
The birthday-bound formula gives the probability that at least two signals in a set of n share the same 64-bit ID:
| Signals | Collision Probability |
|---|---|
| 1,000 | ~2.7 × 10−14 |
| 100,000 | ~2.7 × 10−10 |
| 1,000,000 | ~2.7 × 10−8 (1 in 37 million) |
At the throughput of a LoRa mesh network, these volumes would take years to accumulate. Collisions are negligible.
6. Compatibility & Migration
Stock firmware compatibility
Socialmesh extensions require no firmware changes. The relationship is straightforward:
| Component | Awareness | Behavior |
|---|---|---|
| Stock Meshtastic firmware | None | Routes and relays SM packets normally |
| Stock Meshtastic app | None | Silently ignores SM portnums |
| Older Socialmesh app | Partial | Handles portnum 256 (legacy JSON) only |
| Current Socialmesh app | Full | Handles portnums 256, 260, 261, 262 |
Stock firmware relays packets on portnums 260–262 just as it relays any other packet. It does not need to understand the payload. The routing layer is portnum-agnostic.
Peer capability detection
Socialmesh detects capable peers passively. When a node sends any packet on portnums 260–262, it reveals itself as a Socialmesh node. The app tracks this per-node — no explicit handshake or beacon is needed.
Dual-send migration
During the transition period, the app can operate in dual-send mode: it sends both a binary SM_SIGNAL (portnum 261) for current peers and a legacy JSON signal (portnum 256) for older peers. Both formats carry the same content and map to the same signal ID, so the receiving app deduplicates them into a single item regardless of which encoding arrives first.
Dual-send increases airtime by roughly 38% during the migration window. Once all peers on the mesh are detected as SM-capable, legacy sending can be disabled.
| Mode | Binary (261) | Legacy JSON (256) | When to Use |
|---|---|---|---|
| Legacy only | No | Yes | Before binary support ships |
| Dual-send | Yes | Yes | Mixed mesh, some peers not upgraded |
| Binary only | Yes | No | All peers confirmed SM-capable |
Minimum firmware version: Private portnum support (260–262), the BLE/USB API, and channel encryption have been stable since Meshtastic firmware 2.0. No minimum beyond 2.0 is required.
7. Design Principles
Pure extension, no fork. All Socialmesh logic lives in the app. The firmware is unmodified. This means zero rebase cost when Meshtastic releases a new firmware version, and zero risk of breaking upstream compatibility.
Conservative airtime. Every packet type has app-enforced rate limits. Presence beacons are limited to once per 5 minutes. Signals have a 30-second minimum interval. Emergency signals are capped at 3 per hour. These limits exist to be good neighbors on a shared radio channel.
Graceful degradation. If a receiving node does not understand a Socialmesh packet, nothing bad happens — the packet is silently ignored. If a sending node cannot detect any SM-capable peers, it falls back to legacy JSON. The app never assumes the entire mesh runs Socialmesh.
Byte budgets, not MTU promises. All content limits (63 bytes for presence status, 140 bytes for signal content) are enforced at encoding time, well below the LoRa payload ceiling. This provides headroom for radio overhead and avoids fragmentation.
Client-side expiry. TTL values on signals are display hints for receiving apps. The mesh has no mechanism to enforce expiry or recall a broadcast. This keeps the protocol stateless and avoids complexity in the relay layer.
Deterministic encoding. Binary fields have fixed sizes and positions. There are no variable-length integers, no field tags, no schema negotiation. A decoder needs only the flags byte to know which fields are present.
Version tolerance. Unknown protocol versions and unknown packet kinds are silently dropped. This allows the protocol to evolve without coordination across all nodes on a mesh.
8. For Developers
Interoperability
If you are building an app or tool that operates on a Meshtastic mesh alongside Socialmesh nodes, here is what to expect:
- Portnums 260–262 will appear in your received packet stream. If you do not handle them, ignore them — they are well-formed Meshtastic packets with opaque binary payloads.
- Portnum 256 carries JSON-encoded signals for backward compatibility. If you already handle portnum 256, you may see both a JSON and a binary version of the same signal during the dual-send period. Deduplicate by signal ID if needed.
- No collision risk with your portnums unless you also use 260–262. The Meshtastic private range (256–511) is first-come, first-served.
Decoding SM packets
Every SM packet starts with a one-byte header (hdr0). To identify the
packet type:
version = (hdr0 >> 4) & 0x0F
kind = hdr0 & 0x0F
If version is higher than you support, discard the packet. If
kind is not one you recognize, discard the packet. Otherwise, read the
flags byte (byte 1) and decode the remaining fields based on the kind.
All multi-byte integers are big-endian. All strings are UTF-8 with a one-byte length prefix counting bytes, not characters.
Rate limit expectations
| Packet Type | Minimum Interval |
|---|---|
| Presence beacon | 5 minutes |
| Signal broadcast | 30 seconds |
| Identity broadcast | 30 minutes |
| Identity request | 10 minutes (per target node) |
| Emergency signal | 3 per hour max |
These are app-side limits, not mesh-enforced. A misbehaving implementation could exceed them, but the firmware's duty cycle enforcement provides a hard backstop regardless.
This page is the public specification for the Socialmesh mesh protocol. Questions or interoperability concerns? Reach out via the support page.