The Reticulum transport provides long-range, resilient mesh networking via the Reticulum network stack. It supports LoRa, TCP, UDP, serial, I2P, and other mediums, making it ideal for off-grid communication, disaster recovery, and infrastructure-sparse environments where BLE range is insufficient and Internet connectivity is unavailable.
Reticulum is one of five transports in the Offline Protocol SDK, alongside BLE, Wi-Fi Direct, Internet and Nostr. It is disabled by default because it requires external infrastructure (a running Reticulum instance, an RNode radio, or a gateway).
This repository ships the device half. The Rust transport opens no Reticulum link of its own: it manages queues, metrics and the confirmation loop, and expects the platform to bridge to a real Reticulum stack. Both mobile managers now speak the gateway daemon contract to a configurable address: they attach with a signed address declaration, settle each send on the gateway's verdict, and watch presence. What answers on the other end is a gateway daemon built to that contract, which is a deployment rather than something this SDK ships. With nothing listening at
daemonAddress, enabling Reticulum gives you a transport that never becomes available.
| Scenario | Why Reticulum |
|---|---|
| Off-grid / wilderness | LoRa reaches 2-15+ km line-of-sight, far beyond BLE's ~50m |
| Disaster response | Works without cell towers, Internet, or power infrastructure |
| Rural / sparse networks | Bridges gaps where devices are too far apart for BLE mesh |
| Censorship resistance | I2P transport option for anonymized routing |
| Hardware-constrained setups | RNode devices are inexpensive and self-contained |
Reticulum is not suitable for:
- High-bandwidth transfers (media, files) — typical LoRa throughput is ~0.7 KB/s, peak ~2.7 KB/s
- Low-latency applications — LoRa multi-hop paths can add seconds of latency
- Environments where all devices are within BLE range — BLE is faster and simpler
The Offline Protocol SDK's ReticulumTransport manages message queues, delivery metrics, and the confirmation loop on the Rust side. The platform layer is responsible for bridging to an actual Reticulum instance. There are several ways to achieve this bridge, each with different trade-offs (see Integration Strategies below).
┌──────────────────────┐
│ Offline Protocol │
│ (Rust Core) │
│ │
│ ReticulumTransport │◄── Transport trait implementation
│ - send_queue │ (same interface as BLE, WiFi, Internet)
│ - receive_queue │
│ - pending_confirm │
│ - metrics │
└──────────┬───────────┘
│ Platform Bridge (UniFFI)
▼
┌──────────────────────┐
│ Platform Layer │
│ (iOS/Android) │
│ │
│ Integration via: │
│ - Embedded Python │◄── Most proven (Sideband pattern)
│ - reticulum-rs │◄── Pure Rust (emerging)
│ - HDLC IPC bridge │◄── Desktop/server only
│ - TCP gateway │◄── Via TCPClientInterface
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Reticulum Stack │
│ │
│ Interfaces: │
│ - RNode (LoRa) │
│ - TCP / UDP │
│ - I2P │
│ - Serial / KISS │
│ - AutoInterface │
└──────────────────────┘
Reticulum is a Python-first project. The reference implementation (pip install rns) is the authoritative definition of the protocol. There is no stable C API or documented wire protocol spec for external programs to use. This has important implications for mobile integration.
How existing Reticulum apps work: Every production Reticulum mobile app (notably Sideband) embeds the full Python runtime and RNS library into the app using frameworks like python-for-android. They do not connect to an external daemon via TCP.
The shared instance IPC (rnsd daemon on port 37428) uses an internal protocol (HDLC-framed Reticulum packets over a Unix domain socket, with TCP as fallback). This is designed for multiple Python programs on the same host to share one Reticulum instance — not as a general-purpose API for non-Python apps.
This means the platform bridge you implement must choose one of the strategies described below, depending on your deployment target.
Embed the Python runtime and RNS library directly in your mobile app. This is how Sideband works and is the most battle-tested approach.
Android: Use Chaquopy or python-for-android to embed a Python interpreter. Call import RNS from embedded Python and bridge to your native code.
iOS: Use Kivy-iOS or a similar Python embedding framework. Note that iOS Reticulum support is still maturing — currently limited to TCP and multicast UDP interfaces (no BLE/serial yet).
Pros: Full Reticulum protocol support, proven by Sideband Cons: Adds ~30MB+ for the Python runtime, complex build setup
The reticulum crate by BeechatNetworkSystemsLtd is a Rust port of the Reticulum protocol stack. It was presented at FOSDEM 2026 and is under active development.
If reticulum-rs achieves full wire-format compatibility with the Python reference, it could be linked directly into the Offline Protocol SDK with zero Python dependency. This is the ideal long-term path.
Pros: Native Rust, no Python dependency, small binary (<1MB) Cons: Still maturing; wire-format interoperability with the Python reference is not yet guaranteed
On desktop/server platforms where rnsd is running, a non-Python app can connect to the shared instance socket and exchange HDLC-framed raw Reticulum packets:
- Linux/macOS: Unix domain socket (abstract socket
\0rns/default) - TCP fallback:
127.0.0.1:37428(used when domain sockets are unavailable, or whenshared_instance_type = tcpis set in config)
The HDLC framing uses 0x7E flag bytes with 0x7D escape byte-stuffing. The payloads are raw Reticulum wire-format packets.
Critical caveat: This gives you raw packet-level access only. You still need to implement Reticulum's cryptography (X25519, Ed25519, AES-256-CBC, HMAC-SHA256), identity management, destination resolution, and link establishment yourself. The shared instance is a packet relay, not a high-level messaging API.
Pros: Lightweight, no Python in your process, uses system daemon Cons: Desktop only, requires reimplementing Reticulum protocol logic, undocumented IPC protocol
Connect to a remote Reticulum transport node using Reticulum's TCPClientInterface. In this model, a Reticulum gateway server handles the protocol complexity, and your app communicates with it over a standard TCP connection.
This requires a Reticulum node configured as a TCP server that your app connects to as a client. The Reticulum transport node handles identity, routing, and encryption.
Pros: Standard TCP, works on any platform, no local daemon required Cons: Requires a remote gateway server, adds network dependency
React Native (TypeScript):
const protocol = new OfflineProtocol({
appId: 'my-app',
profile: 'user123',
transports: {
ble: { enabled: true },
reticulum: { enabled: true },
},
});Kotlin (Android):
val config = ProtocolConfig(
appId = "my-app",
profile = "user123",
bleEnabled = true,
reticulumEnabled = true,
// ... other fields
)Swift (iOS):
let config = ProtocolConfig(
appId: "my-app",
profile: "user123",
bleEnabled: true,
reticulumEnabled: true,
// ... other fields
)| Constant | Value | Description |
|---|---|---|
| Connection timeout | 60s | Enforced by the native managers, which each hold their own constant. Rust holds none: it opens no transport socket. |
| Pending confirmation timeout | 120s | Time before treating an unconfirmed send as failed (vs 15s for Internet). Enforced in Rust. |
| Max frame size | gateway-set | The gateway refuses an oversized frame with a frame_too_large verdict, so the limit is the one your gateway is configured with rather than an SDK constant. Rust enforces only the transport-wide DEFAULT_MAX_MESSAGE_SIZE on inbound bytes. |
| Reticulum encrypted MDU | 383 bytes | Single-packet maximum for encrypted data; plain MDU is 465 bytes |
| Reticulum MTU | 500 bytes | Total wire-format maximum including headers |
The longer timeouts reflect the high-latency reality of LoRa multi-hop paths.
Regardless of which integration strategy you choose, the platform bridge interacts with ReticulumTransport through the same UniFFI API:
- Initialize your Reticulum integration (embedded Python,
reticulum-rs, a gateway daemon connection, and so on) - Attach, if you speak the gateway contract:
gatewayAddressDeclaration(challenge)builds the proof,reticulumAddressDeclared(address)andreticulumAddressDeclarationRefused(reason)report the answer, andreticulumGatewayCapabilities(tokens)hands over the advertisement - Report status via
reticulumStatusChanged(true)once the session is bound, not when the socket opens - Poll for outgoing messages via
reticulumGetNextMessage()in a loop - Send each message through your Reticulum integration
- Settle each send on the answer:
reticulumConfirmSent(messageId), orreticulumSendFailedWithReason(messageId, reason)so arecipient_unreachableverdict can park the message - Receive incoming messages and pass them to
reticulumMessageReceived(senderId, data) - Watch presence by polling
reticulumPresenceWatchlist()and reporting answers throughreticulumPeerPresence(peerId, online, lastSeenMs) - Report disconnection via
reticulumStatusChanged(false)on connection loss, having first failed every unanswered frame - Reconnect with backoff (the bundled managers use 1s doubling to 30s)
Normative home: this protocol is specified in the gateway contract, which is the document to implement against. What follows describes what the managers do, which is that contract.
Note what this section does not say:
rnsddoes not speak this protocol. Its own shared-instance IPC is HDLC-framed Reticulum packets over a Unix domain socket (Strategy 3 above), not this. A gateway daemon is a separate program that attaches to a Reticulum stack on one side and speaks this contract to devices on the other.
The built-in ReticulumManager (iOS and Android) speaks a newline-delimited JSON protocol over TCP to a configurable daemonAddress (default localhost:4242). Both platforms implement the same message types to stay in sync.
Client-to-daemon messages:
| Type | Fields | Description |
|---|---|---|
Identify |
device_id (string), protocol_version (int) |
Sent immediately after TCP connect. device_id is this device's off1… address where one exists, but it is an unverified claim: only DeclareAddress binds a session. |
DeclareAddress |
address (string), public_key (base64), signature (base64) |
Proves this device holds the address it claims, over the gateway's per-connection challenge. The SDK builds and signs it; the manager only frames it. |
SendMessage |
recipient (string), content (base64), encoding ("base64"), message_id (string), reply_to_msg (string, optional) |
Submit one frame. The message_id is the SDK's own, and is what the verdict is correlated by. |
CheckPresence |
peers (array of string) |
Ask about the peers the SDK is waiting to hear about. One frame for the batch, capped at 64 peers. |
Daemon-to-client messages:
| Type | Fields | Description |
|---|---|---|
Challenge |
challenge (base64, 32 bytes), protocol_version (int) |
Minted per connection. A challenge of any other length is refused rather than signed. |
AddressDeclared |
address (string) |
The address the gateway bound. Checked against local_address(): a mismatch is a GATEWAY_ADDRESS_BINDING_MISMATCH security warning and closes the connection. |
AddressError |
reason (string) |
The declaration was refused. Emits GATEWAY_ADDRESS_DECLARATION_REFUSED and closes the connection; the carrier is never announced. |
Capabilities |
tokens (array of string) |
Bounded at 64 tokens of 128 bytes and handed to the SDK before the carrier is announced. |
MessageSent |
message_id (string), recipient (string) |
The frame was forwarded. Confirms that id, and nothing else. |
DeliveryError |
message_id (string), recipient (string), reason (string) |
The frame was not forwarded. The reason travels to the SDK verbatim; a recipient_unreachable prefix parks the message and offers it to the mesh. |
MessageReceived |
sender (string), content (string), encoding (optional "base64") |
An incoming message from a remote peer. |
PresenceStatus |
peer (string), online (bool), last_seen_ms (int, optional) |
Answers a CheckPresence, or arrives unsolicited when a watched peer's state changes. |
StatusUpdate |
status (string) |
connected completes the attach and announces the carrier. Others are logged. |
All messages are JSON objects terminated by a newline (\n). Each TCP read is buffered and split on newlines to handle partial reads; a line longer than 1 MiB abandons the connection, because its tail would otherwise be read as a fresh line and every frame after it would be garbage.
→ Identify → DeclareAddress
← Challenge ← AddressDeclared | AddressError
← Capabilities
← StatusUpdate(connected) ← the carrier is announced here
The TCP connection is not the transport. A session the gateway has not
bound is verdict-only on the other side: it may submit and be told
attach_required, and it is never registered as a recipient, so nothing
addressed to this device would arrive over it. The managers therefore announce
the carrier only on StatusUpdate(connected) with a bound session, and close
the connection on a refusal rather than offering a transport that can only
refuse. This is where the Reticulum managers deliberately differ from the
Internet manager, which reports up after a refused declaration because the
relay keeps delivering on established sessions in account-name space.
The signed proof is built in the SDK, not in the bridges: the payload commits
this device's address under offline-gateway-addr-v1 and is pinned by
conformance vectors. A bridge only frames
what gatewayAddressDeclaration() returns.
The send confirmation loop is critical for accurate DORS scoring. Without it, DORS cannot measure Reticulum's actual delivery performance.
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Rust Core │ poll │ Platform │ send │ Reticulum │
│ send_queue │───────►│ Bridge │───────►│ Stack │
│ │ │ │ │ │
│ pending_ │◄───────│ confirm/ │◄───────│ delivery │
│ confirmation│ report │ fail │ status │ callback │
└─────────────┘ └──────────────┘ └──────────────┘
Important: Messages enter pending_confirmation state when dequeued by reticulumGetNextMessage(). The platform must settle every one of them, and against a gateway it settles on the gateway's verdict rather than on the socket write: a successful write means the gateway has the bytes, which says nothing about whether it could forward them.
Unconfirmed messages expire after 120 seconds and are counted as failures. The bundled managers give up on a verdict at 60 seconds and fail the frame themselves, deliberately the shorter of the two clocks: were it the longer one, the core would expire the frame first and the verdict would then settle an id it had already moved past. They also cap frames in flight at 8, and refuse to re-send an id that is still outstanding: the core re-queues an unconfirmed frame after its own acknowledgement timeout, and sending it again would forward it twice and later fail an id the gateway had already confirmed.
This shows the SDK-facing bridge logic. The actual Reticulum communication (sendViaReticulum / receiveFromReticulum) depends on your chosen integration strategy.
class ReticulumBridge(
private val protocol: OfflineProtocol,
private val scope: CoroutineScope
) {
fun onReticulumReady() {
protocol.reticulumStatusChanged(true)
scope.launch(Dispatchers.IO) { sendLoop() }
}
fun onReticulumDisconnected() {
protocol.reticulumStatusChanged(false)
}
private suspend fun sendLoop() {
while (isActive) {
val next = protocol.reticulumGetNextMessage()
if (next != null) {
try {
// Submit, then settle when the answer comes back. Against
// a gateway, confirming here would settle a frame that may
// yet be refused, and would hide the one verdict that
// parks a message, `recipient_unreachable`.
submitViaReticulum(next.messageId, next.data.toByteArray())
} catch (e: Exception) {
protocol.reticulumSendFailedWithReason(next.messageId, "Write failed")
}
} else {
delay(100)
}
}
}
fun onDataReceived(senderId: String, data: ByteArray) {
protocol.reticulumMessageReceived(senderId, data.map { it.toUByte() })
}
/** The gateway's verdict for one submitted frame. */
fun onVerdict(messageId: String, reason: String?) {
if (reason == null) {
protocol.reticulumConfirmSent(messageId)
} else {
// Verbatim: the core matches the `recipient_unreachable` prefix
// and discards the rest at that boundary.
protocol.reticulumSendFailedWithReason(messageId, reason)
}
}
// Implement based on your chosen strategy:
// - Embedded Python: call RNS.Packet.send() via Chaquopy
// - reticulum-rs: call the Rust crate directly
// - Gateway daemon: write a SendMessage line carrying this messageId
private suspend fun submitViaReticulum(messageId: String, data: ByteArray) { /* ... */ }
}DORS evaluates Reticulum alongside all other transports using the same multi-factor scoring system. Reticulum's scoring profile reflects its characteristics:
| Factor | Weight | Rationale |
|---|---|---|
| Reliability | 30% | Most important — if Reticulum delivers, that's what matters |
| Energy | 25% | LoRa is relatively energy-efficient |
| Proximity | 20% | Hop count matters on multi-hop LoRa paths |
| Congestion | 15% | Queue pressure signals overload |
| Signal | 5% | RSSI from radio interfaces (when available) |
| Bandwidth | 5% | Low weight because bandwidth is inherently limited |
| Parameter | Value | Description |
|---|---|---|
| Base score | 0 | No bonus — must earn selection on merit |
| Media penalty | -40 | Strongly penalized when message requests transport_preference = "internet" |
| Bandwidth max | 2,700 B/s | LoRa peak throughput at SF7/BW500kHz for normalization |
| Bandwidth default | 20 | Conservative default score when no measurement available |
| Energy baseline | 75 | Between BLE (90) and Internet (60) |
| High-power | No | Not penalized during low battery |
| Tie-break priority | 3 (second-lowest) | Internet (0) > WiFi Direct (1) > BLE (2) > Reticulum (3) > Nostr (4) |
Actual throughput depends heavily on LoRa parameters. These are raw bitrates before Reticulum protocol overhead:
| Configuration | Raw Bitrate | Effective Throughput | Range |
|---|---|---|---|
| SF7 / BW500kHz / CR4:5 | ~21.9 kbps | ~2.7 KB/s | Short |
| SF7 / BW125kHz / CR4:5 | ~5.5 kbps | ~0.67 KB/s | Medium |
| SF8 / BW125kHz / CR4:5 | ~3.1 kbps | ~0.38 KB/s | Long |
| SF12 / BW125kHz / CR4:5 | ~0.29 kbps | ~0.04 KB/s | Maximum |
Higher spreading factors (SF) increase range at the cost of throughput. The SDK uses 2,700 B/s as the peak normalization value for DORS scoring.
Reticulum will be selected when:
- Other transports are unavailable (Internet down, no BLE peers in range, no WiFi Direct)
- Reticulum has significantly better reliability scores than degraded alternatives
- Battery is low and high-power transports (WiFi Direct, Internet) are penalized
Reticulum will not be selected when:
- Higher-bandwidth transports are available with comparable reliability
- The message requests Internet preference (media/file transfers)
- Scores are tied (tie-break favors Internet > WiFi > BLE > Reticulum > Nostr)
Reticulum uses BLE-like chunk sizes for file transfers due to its low bandwidth:
| Parameter | Value | Comparison |
|---|---|---|
| Chunk size | BLE chunk size | Same as BLE (smaller chunks for low throughput) |
| Media window | BLE media window | Same as BLE |
| Media transfer eligibility | Excluded | Reticulum is not in the preferred transport list for media transfers |
Large file transfers over Reticulum are technically possible but will be very slow. The SDK automatically excludes Reticulum from the preferred media transfer transport list. If Reticulum is the only available transport, transfers will still work but at LoRa speeds.
The Offline Protocol SDK does not include a Reticulum stack — you must provide one via your chosen integration strategy. Below is reference information for setting up the Python Reticulum daemon, which is useful for desktop deployments and as a gateway for mobile apps.
pip install rns # Standard installation
pip install rnspure # Dependency-free variant for constrained systemsThis installs the daemon (rnsd) and CLI tools (rnstatus, rnpath, rnprobe, rncp, rnx, rnodeconf, rnid).
An RNode is a LoRa transceiver that runs open-source firmware. RNode hardware includes:
- ESP32-based: LilyGO T-Beam, T3S3, LoRa32; Heltec LoRa32; Unsigned RNode v2.x
- nRF52-based: RAK4631, LilyGO T-Echo, Heltec T114
Connection methods: USB serial, Bluetooth, or WiFi/TCP.
Radio bands: 433 MHz, 868 MHz, 915 MHz, and 2.4 GHz ISM bands.
The Reticulum configuration lives at ~/.reticulum/config (created on first run). A minimal configuration for LoRa:
[reticulum]
enable_transport = True
share_instance = Yes
[logging]
loglevel = 4
[interfaces]
[[Default Interface]]
type = AutoInterface
enabled = Yes
[[RNode LoRa Interface]]
type = RNodeInterface
port = /dev/ttyUSB0
frequency = 867200000
bandwidth = 125000
txpower = 7
spreadingfactor = 8
codingrate = 5Reticulum supports many interface types. The most relevant:
| Interface | Use Case |
|---|---|
RNodeInterface |
LoRa via RNode hardware (USB/BLE/WiFi) |
AutoInterface |
Automatic local LAN discovery |
TCPClientInterface |
Connect to a remote Reticulum TCP server |
TCPServerInterface |
Accept incoming TCP connections |
UDPInterface |
UDP transport |
I2PInterface |
Anonymized routing over I2P |
SerialInterface |
Raw serial port |
KISSInterface |
KISS TNC protocol |
PipeInterface |
Named pipe / stdin/stdout |
rnsd # Start daemon (foreground)
rnsd --service # Start as background service
rnstatus # Check interface status
rnstatus -a # Show all interfaces
rnpath -t # Show routing tableWhen acquiring multiple locks in ReticulumTransport, follow this order to prevent deadlocks:
statuspending_confirmationsend_queuemetricsreceive_queueplatform_handle
This is documented in the source at crates/offline-protocol-transport/src/reticulum.rs.
Reticulum reports the same TransportMetrics as other transports:
| Metric | Source | Notes |
|---|---|---|
rssi |
Radio interface (if available) | LoRa RSSI from RNode |
latency_ms |
Platform measurement | Round-trip through Reticulum stack |
bandwidth_bps |
Platform estimate | ~700 typical (SF7/BW125), ~2,700 peak (SF7/BW500) |
congestion |
Auto-calculated | Based on send queue depth |
queue_depth |
Auto-tracked | Current send queue size |
success_count |
Confirmation loop | Incremented on confirmSent |
failure_count |
Confirmation loop | Incremented on reportSendFailure or timeout |
delivery_ratio |
Auto-calculated | success / (success + failure) |
The update_metrics method preserves confirmation loop counts (success/failure) when the platform reports new metrics, preventing count resets.
Reconnection is owned entirely by the native managers and configured from the app's transport config. Rust holds no reconnection state: the transport opens no socket, so a retry budget there would describe a connection it does not hold.
| Parameter | Where it lives | Default | Description |
|---|---|---|---|
daemonAddress |
app config (transports.reticulum) |
localhost:4242 |
Host and port of the daemon |
autoReconnect |
app config | true |
Reconnect after disconnection |
maxReconnectAttempts |
app config | 0 (infinite) |
Attempts before giving up |
| Backoff | native managers | 1s doubling to 30s | Not configurable |
On disconnection:
- All pending confirmations are failed immediately
- Send queue is preserved (messages will be sent after reconnection)
- Reconnection attempts begin after the current backoff interval
- The reconnect counter and backoff reset once the gateway binds the session, not on the TCP open: a refused declaration climbs the ladder like a failed connect
The socket is open and the session is not bound. Every submission draws
attach_required from the gateway and nothing addressed to this device is
delivered, which is why the carrier is deliberately not offered.
- Look for a
security_warningevent.GATEWAY_ADDRESS_DECLARATION_REFUSEDmeans the gateway rejected the proof;GATEWAY_ADDRESS_BINDING_MISMATCHmeans it bound an address this device does not hold, which has no benign reading. - Check the device has an identity at all. Before MLS storage is initialized there is no address to declare, so the connection is closed and the reconnect ladder keeps trying (with the default unlimited attempts; a configured maximum counts these closes); the carrier becomes available on the first attach after the identity exists.
- Check the gateway mints a 32-byte challenge. Any other length is refused rather than signed.
- Check the gateway sends
StatusUpdatewith statusconnected, afterAddressDeclared. The attach completes on that frame and times out after 10 seconds without it; aconnectedthat arrives before the session is bound closes the connection.
- Verify the Reticulum stack is running:
rnstatus - Check that the shared instance is enabled:
share_instance = Yesin~/.reticulum/config - Check daemon logs:
~/.reticulum/logfile - For TCP gateway: verify the remote host is reachable and the port is open
- Check that every message is settled:
reticulumConfirmSentorreticulumSendFailedWithReason, on the gateway's verdict rather than on the socket write - Verify Reticulum has active interfaces:
rnstatus -a - Check if pending confirmations are timing out (120s) — may indicate the Reticulum stack is not reporting delivery
- Monitor DORS transport switch events — Reticulum may be deprioritized if other transports score higher
- Check LoRa signal quality (RSSI) — move devices or adjust antenna
- Lower
spreadingfactorin Reticulum config for higher throughput (at cost of range) - Increase
txpowerif permitted by regulations - Check for radio interference on the frequency band
- Verify both ends are using compatible LoRa parameters (frequency, bandwidth, SF)
- Verify
reticulumEnabled: truein config - Verify the session actually attached. Against a gateway,
reticulumStatusChanged(true)fires only once the address declaration is bound, so a transport that never becomes available usually means a refused or unanswered handshake. Look for aGATEWAY_ADDRESS_DECLARATION_REFUSEDorGATEWAY_ADDRESS_BINDING_MISMATCHsecurity warning - Check DORS scores — Reticulum has a low tie-break priority (only Nostr is lower), so it needs to outscore alternatives
- Reticulum is excluded from media transfers by design
- Reticulum Network — Official Reticulum documentation
- Reticulum GitHub — Reference implementation (Python)
- reticulum-rs — Rust port (emerging)
- Sideband — Reference mobile Reticulum messenger
- RNode — LoRa transceiver hardware
- RNode Firmware — Open-source RNode firmware
- Transport Architecture — How all transports fit together
- DORS Deep Dive — Transport selection algorithm
- DORS Configuration — Tuning transport selection
- Configuration Guide — All SDK configuration options