A multi-interface Reticulum gateway firmware for ESP32-series microcontrollers. Routes packets transparently between WiFi (UDP), ESP-NOW, Serial KISS, Bluetooth Classic, LoRa, and amateur-radio (AX.25/APRS) interfaces.
Status: Software release candidate for controlled pilots. Core routing, ESP-NOW mesh, and KISS-over-USB TNC mode have been exercised on ESP32-C3 hardware. Broad or unattended production deployment remains blocked on the hardware-security, HIL, manufacturing, and regulatory gates in docs/PRODUCTION_READINESS.md.
- Multi-interface bridging — packets received on any interface are forwarded to all others based on routing table or broadcast
- ESP-NOW mesh — automatic peer discovery via announce packets; zero-config wireless mesh between ESP32 nodes
- KISS-over-USB TNC — use an ESP32-C3 as a USB-connected TNC for desktop Reticulum instances
- Announce-based routing — distance-vector routing with 16-byte destination hashes, hop counting, and automatic route expiry
- Authenticated encrypted links — Reticulum-compatible X25519/Ed25519 handshake and Fernet session encryption
- LoRa support — long-range radio via SX1278-compatible modules (Heltec LoRa32 v3)
- Experimental HAM radio — AX.25/APRS framing and AFSK components for development builds; not production-qualified
- HTTP REST API — optional runtime configuration, signed OTA updates, and metrics endpoint
- Multi-platform — builds for ESP32, ESP32-C3, ESP32-S2, ESP32-S3, and Heltec LoRa boards
| Board | Environment | Notes |
|---|---|---|
| ESP32-C3-DevKitM-1 | esp32-c3-prod-managed |
Canonical managed production target |
| ESP32-C3 (KISS TNC) | esp32-c3-kiss-usb |
USB = KISS port, debug on UART1 GPIO2/4 |
| ESP32 (original) | esp32dev |
Bluetooth Classic available |
| ESP32-S2-Saola-1 | esp32-s2-devkitm-1 |
|
| ESP32-S3-DevKitC-1 | esp32-s3-devkitc-1 |
|
| TTGO T-OI Plus | ttgo-t-oi-plus |
|
| Heltec LoRa32 v3 | heltec_wifi_lora_32_V3 |
LoRa + WiFi + ESP-NOW |
- PlatformIO (VS Code extension or CLI)
- ESP32 development board
- USB cable
git clone https://github.com/AkitaEngineering/ESP32-C3-Reticulum-Node
cd ESP32-C3-Reticulum-Node
# Build the canonical managed production environment
pio run
# Flash a dev/debug build to a connected board
pio run -e esp32-c3-devkitm-1 -t upload
# Monitor serial output (115200 baud)
pio device monitor -b 115200Generate a unique per-device config with WiFi credentials, API token, and OTA public key. Provision the generated file as /config.json in SPIFFS before the unit is connected to its field network. Production firmware deliberately disables unauthenticated network bootstrap.
./tools/make_device_config.py --device-id RNS-000001 --node-name field-001 \
--wifi-ssid field-net --wifi-password "$WIFI_PASSWORD" \
--api-public-key "$OTA_PUBLIC_KEY"- Flash two boards with the default environment (same WiFi credentials)
- Power both boards and open serial monitors
- Within about 35 seconds they exchange announce packets and add each other as ESP-NOW peers
- Packets sent to one node's destination hash are relayed wirelessly to the other
Use an ESP32-C3 as a USB-attached KISS TNC for a desktop Reticulum instance:
pio run -e esp32-c3-kiss-usb -t uploadAfter flashing, press the physical RESET button (the ESP32-C3 USB Serial/JTAG auto-reset always enters bootloader mode; a manual reset is required to boot into application firmware).
In this mode:
- USB CDC carries KISS-framed Reticulum packets (the TNC interface)
- UART1 (GPIO2 RX / GPIO4 TX) carries debug output at 115200 baud
- Packets received via USB KISS are forwarded over ESP-NOW (and vice versa)
Important: On the host side, you must configure Reticulum to use a KISSInterface, not a
SerialInterface.SerialInterfaceexpects HDLC framing (0x7E), while the ESP32 firmware sends KISS framing (0xC0). If you useSerialInterfaceyou will see "malformed packet" errors.
Example Reticulum configuration (~/.reticulum/config):
[[ESP32_KISS]]
type = KISSInterface
enabled = yes
port = /dev/ttyACM0
speed = 115200ESP32-C3 USB Note: The HWCDC
connectedflag startsfalse. The host must write at least one byte to the device beforeSerial.write()output will flow. Most serial tools do this automatically; for rawopen()calls, write a newline after connecting.
| Environment | Purpose |
|---|---|
esp32-c3-prod-managed |
Default/canonical: USB KISS, ESP-NOW, WiFi UDP, authenticated API, metrics, signed OTA |
esp32-c3-devkitm-1 |
Default dev/debug build (USB CDC, debug enabled) |
esp32-c3-kiss-usb |
KISS TNC over USB CDC |
esp32-c3-demo-sender |
Sends periodic test packets (demo mode) |
esp32-c3-prod |
Production build (-Os, no debug, no demo traffic) |
esp32-c3-prod-usb |
Production USB/KISS build with runtime JSON naming config enabled |
esp32-c3-prod-metrics |
Production + metrics/heartbeat endpoint |
esp32-c3-web |
HTTP REST API, OTA, JSON config |
esp32-c3-debug |
WiFi disabled, -Og debug optimizations |
esp32-c3-minimal |
Minimal main (build sanity check) |
esp32-c3-kiss-usb-test |
Progressive USB subsystem isolation test |
esp32dev |
Original ESP32 target |
esp32-s2-devkitm-1 |
ESP32-S2 target |
esp32-s3-devkitc-1 |
ESP32-S3 target |
ttgo-t-oi-plus |
TTGO T-OI Plus (web + OTA) |
heltec_wifi_lora_32_V3 |
Heltec LoRa32 v3 (LoRa enabled) |
Build the default environment: pio run
Build a specific environment:
pio run -e esp32-c3-devkitm-1The default production target is esp32-c3-prod-managed. It reads /config.json from SPIFFS before network initialization.
node_name: required for managed production; use a unique safe name of 1–47 characters. Offline builds may omit it to use the MAC-derived default such asrns-6497AC.rns_app_name: required for managed production; use a safe Reticulum application name of 1–63 characters.wifi.ssid/wifi.password: boot-time WiFi station credentials.api.token: required per-device API token (at least 32 characters).api.public_key: required 32-byte Ed25519 OTA public key in hex.data/config.example.json: editable template only; itsCHANGE_MEvalues and empty OTA key are deliberately rejected by managed production firmware.
For a factory filesystem upload, temporarily stage the generated file as the ignored data/config.json, upload it, and remove the staged copy:
cp provisioning/devices/RNS-000001.config.json data/config.json
pio run -e esp32-c3-prod-managed -t uploadfs --upload-port /dev/serial/by-id/<board>
rm data/config.jsonDo not commit generated configs or manifests; both contain credentials and are ignored by Git.
include/ Header files (Config.h, all module headers)
src/ Firmware source code
main.cpp Entry point (setup/loop)
ReticulumNode.cpp Core application controller
InterfaceManager.cpp Interface abstraction & packet routing
KISS.cpp KISS protocol encoder/decoder
ReticulumPacket.cpp Packet serialization/deserialization
RoutingTable.cpp Route storage & announce-based discovery
Link.cpp Authenticated encrypted link sessions
LinkManager.cpp Link lifecycle management
AX25.cpp AX.25 frame encoding
AudioModem.cpp AFSK audio modem
WebServer.cpp HTTP REST API and signed OTA
Config.cpp Runtime JSON configuration
Utils.cpp Utility functions
Winlink.cpp Experimental Winlink-style message adapter
test/ PlatformIO unit tests
tests/ Hardware integration & debug scripts (Python)
kiss_usb_inject.py KISS packet injector (raw fd, no DTR/RTS reset)
espnow_debug.py Send debug commands to a node
kiss_roundtrip.py KISS encode/decode round-trip test
send_and_sniff.py Packet TX/RX verification
...
docs/ Technical documentation
tools/ Build, provisioning, and OTA scripts
data/ Example configuration files
All compile-time settings are in include/Config.h. Key parameters:
| Parameter | Default | Description |
|---|---|---|
wifi.ssid / wifi.password |
— | Runtime WiFi credentials in /config.json |
RNS_UDP_PORT |
4242 | Reticulum UDP transport port |
ANNOUNCE_INTERVAL_MS |
30000 | Announce broadcast interval (ms) |
MAX_HOPS |
128 | Maximum packet hop count |
MAX_ROUTES |
20 | Maximum routing table entries |
KISS_SERIAL_SPEED |
115200 | KISS UART baud rate |
Build flags control feature inclusion:
| Flag | Effect |
|---|---|
-DDEBUG_ENABLED=1 |
Enable verbose serial debug output |
-DKISS_OVER_USB=1 |
USB CDC = KISS port, debug on UART1 |
-DDEMO_TRAFFIC_ENABLED=1 |
Send periodic test packets |
-DWEBSERVER_ENABLED=1 |
Enable the HTTP REST API |
-DJSON_CONFIG_ENABLED=1 |
Runtime JSON config (SPIFFS) |
-DOTA_ENABLED=1 |
Signed OTA firmware updates |
-DLORA_ENABLED=1 |
Enable LoRa radio interface |
-DHAM_MODEM_ENABLED=1 |
Enable experimental HAM components; rejected by PRODUCTION_BUILD |
-DMETRICS_ENABLED=1 |
Enable /metrics endpoint |
-DBLE_PROVISIONING_ENABLED=1 |
Reserved for BLE GATT provisioning; not included in production builds |
Send a local command packet (context 0xB0) over the serial or Bluetooth KISS interface. This node-specific context is consumed locally and is never forwarded. Payload format: [8-byte dest address (zeros = self)] [ASCII command]
| Command | Response |
|---|---|
routes |
Print the current routing table |
peers |
Print registered ESP-NOW peers |
| (empty payload) | Ping — node prints "alive" confirmation |
| Document | Description |
|---|---|
| docs/ARCHITECTURE.md | System architecture and data flows |
| docs/PACKET_FORMATS.md | Reticulum wire format specification |
| docs/KISS_INTERFACE.md | KISS protocol implementation details |
| docs/LINK_LAYER.md | Authenticated encrypted link sessions and limitations |
| docs/API.md | HTTP REST API reference |
| docs/HAM_MODEM.md | Amateur radio interface guide |
| docs/IPFS_INTEGRATION.md | IPFS content addressing |
| docs/ENHANCED_FEATURES.md | Advanced features documentation |
| docs/TECHNICAL_SPECIFICATION.md | Full technical specification |
| docs/ROADMAP.md | Project roadmap |
The ESP32-C3's built-in USB Serial/JTAG controller has some important behaviors:
- Auto-reset enters bootloader: Toggling DTR/RTS (as PlatformIO and most serial tools do) resets the chip into download/bootloader mode, not application mode. After flashing, you must press the physical RESET button to run the firmware.
- HWCDC connected flag: The USB CDC
connectedflag startsfalse. Output viaSerial.write()is silently discarded until the host writes data to the device (triggering theOUT_RECV_PKTinterrupt). Most serial monitors do this automatically on open. - Port numbering: USB port numbers (
/dev/ttyACMx) may change after re-enumeration. Useudevadm infoor check the MAC-based serial number to identify boards.
For host-side capture, pyserial is more reliable than a raw open() because asserting DTR makes the USB CDC connection become active consistently. Keep RTS low to avoid unwanted reset behavior:
import serial
ser = serial.Serial(port, 115200, dsrdtr=False, rtscts=False)
ser.dtr = True
ser.rts = FalseCompile unit-test firmware without an attached board:
pio test -e test --without-uploading --without-testingRun unit tests on a connected board:
pio test -e test --test-port /dev/serial/by-id/<board>Integration tests (require connected hardware):
python tests/kiss_usb_inject.py # KISS relay: PC -> USB -> Board A -> ESP-NOW -> Board B
python tests/espnow_debug.py # Send debug commands to a node
python tests/kiss_roundtrip.py # KISS encode/decode verificationSee CONTRIBUTING.md for guidelines. PRs should be small and focused, include tests where applicable, and compile with pio run -e esp32-c3-devkitm-1 before submission.
See SECURITY.md for responsible disclosure policy and deployment guidance (Secure Boot, Flash Encryption, signed OTA).
See LICENSE for details.
Akita Engineering — github.com/AkitaEngineering