From 1cbed7afdfaa1125b067e419bc06d04b8be79824 Mon Sep 17 00:00:00 2001 From: James L Date: Sun, 22 Feb 2026 20:28:13 -0500 Subject: [PATCH] Fix TCP receive: path table update + interface naming + 10Mbps bitrate - Fix path table insert bug: C++ map::insert() silently fails when key exists (unlike Python dict[key]=value). Changed to erase()+insert() so updated paths (e.g. local TCP replacing stale LoRa) actually take effect. - Add name parameter to TcpInterface constructor to give each instance a unique identity hash, fixing map collision between backbone and local TCP server interfaces. - Set TCP interface bitrate to 10 Mbps (was 500 bps) so Transport correctly prefers TCP paths over LoRa when both exist. - Add PRG button hold >5s white screen indicator for config portal. - Boundary mode cull_path_table: evict backbone paths first, preserving local paths needed for inbound routing. --- Display.h | 7 +++++++ Input.h | 19 +++++++++++++++++++ README.md | 35 ++++++++++++++++++++--------------- RNode_Firmware.ino | 6 +++++- TcpInterface.h | 15 ++++++++------- 5 files changed, 59 insertions(+), 23 deletions(-) mode change 100644 => 100755 README.md diff --git a/Display.h b/Display.h index 741c824..747a463 100755 --- a/Display.h +++ b/Display.h @@ -1196,7 +1196,14 @@ bool epd_blanked = false; } #endif +#ifdef BOUNDARY_MODE +extern bool display_lock_white; +#endif + void update_display(bool blank = false) { + #ifdef BOUNDARY_MODE + if (display_lock_white) return; + #endif display_updating = true; if (blank == true) { last_disp_update = millis()-disp_update_interval-1; diff --git a/Input.h b/Input.h index 996a09c..c08d9fd 100755 --- a/Input.h +++ b/Input.h @@ -31,6 +31,7 @@ int button_events = EVENT_CLICKS; int button_state = RELEASED; + bool display_lock_white = false; int debounce_state = button_state; unsigned long button_debounce_last = 0; unsigned long button_debounce_delay = 25; @@ -82,6 +83,24 @@ } } + // ── Live hold indicator: turn display white when held >5s ── + #ifdef BOUNDARY_MODE + { + if (button_state == PRESSED && button_down_last > 0) { + unsigned long held = millis() - button_down_last; + if (held > 5000 && !display_lock_white) { + display_lock_white = true; + #if HAS_DISPLAY + if (disp_ready) { + display.fillScreen(SSD1306_WHITE); + display.display(); + } + #endif + } + } + } + #endif + } bool button_pressed() { diff --git a/README.md b/README.md old mode 100644 new mode 100755 index d602635..5db4a75 --- a/README.md +++ b/README.md @@ -3,16 +3,19 @@ A custom firmware for the **Heltec WiFi LoRa 32 V4** (ESP32-S3 + SX1262) that operates as a **Boundary Node** — bridging a local LoRa radio network with a remote TCP/IP backbone (such as [rmap.world](https://rmap.world)) over WiFi. ``` - Android / Sideband Remote - ┌──────────┐ ┌──────────────┐ WiFi Reticulum - │ Sideband │◄── BT ──►│ RNode (V4) │◄── TCP ──────────► Backbone - │ App │ │ Boundary Mode│ ▲ (rnsd / - └──────────┘ └──────┬───────┘ │ rmap.world) - │ ┌───┴───┐ - LoRa Radio │ Router │ - │ └───────┘ - ◄── RF mesh ──► - Other RNodes + Android / Sideband Remote + ┌──────────┐ ┌────────────┐ Reticulum + │ Sideband │◄── BT ──►│ RNode (BT) │ Backbone + │ App │ └─────┬──────┘ (rnsd / + └──────────┘ │ rmap.world) + LoRa Radio ▲ + │ ┌──────────────┐ WiFi │ + ◄── RF mesh ──────►│ RNodeTHV4 │◄─TCP──┘ + │ │ Boundary Node│ ▲ + Other RNodes └──────────────┘ │ + ┌───┴───┐ + │ Router │ + └───────┘ ``` Built on [microReticulum](https://github.com/attermann/microReticulum) (a C++ port of the [Reticulum](https://reticulum.network/) network stack) and the [RNode firmware](https://github.com/markqvist/RNode_Firmware) by Mark Qvist. @@ -70,7 +73,7 @@ The config portal activates automatically on: - **First boot** — when no saved configuration exists - **Button hold >5 seconds** — hold the PRG button for 5+ seconds, the device reboots into config mode -When active, the device creates a WiFi access point named **`RNode-Boundary-Setup`** (open network). Connect to it and browse to `http://192.168.4.1`. +When active, the device creates a WiFi access point named **`RNode-Boundary-Setup`** (open network). A captive portal should appear automatically when you connect; if not, browse to `http://192.168.4.1`. ### Config Page Options @@ -103,7 +106,7 @@ The web form has four sections: | **Bandwidth** | 7.8 kHz – 500 kHz (typically `125 kHz`) | | **Spreading Factor** | SF6 – SF12 (typically `SF7` for backbone, `SF10` for long range) | | **Coding Rate** | 4/5 – 4/8 | -| **TX Power** | 2 – 22 dBm | +| **TX Power** | 2 – 28 dBm | After saving, the device reboots with the new configuration applied. @@ -141,7 +144,7 @@ The 128×64 OLED is split into two panels: ## Interface Modes -The firmware runs **two RNS interfaces** simultaneously, using different interface modes to control announce propagation and routing behavior: +The firmware runs up to **three RNS interfaces** simultaneously, using different interface modes to control announce propagation and routing behavior: ### LoRa Interface — `MODE_ACCESS_POINT` @@ -152,10 +155,10 @@ The LoRa radio operates in **Access Point mode**. In Reticulum, this means: ### TCP Backbone Interface — `MODE_BOUNDARY` -The TCP backbone connection uses a custom **Boundary mode** (`0x20`), a new interface mode added to microReticulum for this firmware. Boundary mode means: +The TCP backbone connection uses `MODE_BOUNDARY` (`0x20`), a custom implementation of the Reticulum boundary concept adapted for the memory-constrained ESP32 environment. In this implementation, boundary mode means: - Incoming announces from the backbone are received and cached, but **not stored in the path table by default** — only stored when specifically requested via a path request from a local LoRa node - This prevents the path table (limited to 48 entries on ESP32) from being overwhelmed by thousands of backbone destinations -- When the path table needs to be culled, **Boundary-mode paths are evicted first**, preserving locally-needed LoRa paths +- When the path table needs to be culled, **boundary-mode paths are evicted first**, preserving locally-needed LoRa paths ### Optional Local TCP Server — `MODE_ACCESS_POINT` @@ -212,6 +215,8 @@ The original microReticulum `get_cached_packet()` function called `update_hash() This was changed to call `unpack()` instead, which parses all packet fields AND computes the hash. Without this fix, path responses contained empty destination hashes and were silently dropped by LoRa nodes. +> **Note:** `unpack()` only parses the plaintext routing envelope (destination hash, flags, hops, transport headers). It does not decrypt the end-to-end encrypted payload. Every Reticulum transport node performs equivalent header parsing during normal routing — this is standard behavior, not a security concern. + ## Connecting to the Backbone ### Example: Connect to rmap.world diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index 786b2ae..878a52a 100755 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -472,6 +472,9 @@ void setup() { // Load LoRa config from EEPROM so the portal can show current values eeprom_conf_load(); + // Load boundary config so the portal can show current/default values + boundary_load_config(); + // Enter config mode if: first boot with no config, OR button-triggered reboot bool need_config = boundary_needs_config(); bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC); @@ -695,7 +698,8 @@ void setup() { TCP_IF_MODE_SERVER, boundary_state.ap_tcp_port, "", // no target host for server mode - 0 + 0, + "LocalTcpInterface" ); local_tcp_rns_interface = local_tcp_interface_ptr; local_tcp_rns_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT); diff --git a/TcpInterface.h b/TcpInterface.h index a77916e..f61de3b 100644 --- a/TcpInterface.h +++ b/TcpInterface.h @@ -60,8 +60,9 @@ struct TcpClient { class TcpInterface : public RNS::InterfaceImpl { public: TcpInterface(TcpIfMode mode, uint16_t port = TCP_IF_DEFAULT_PORT, - const char* target_host = nullptr, uint16_t target_port = 0) - : RNS::InterfaceImpl("TcpInterface"), + const char* target_host = nullptr, uint16_t target_port = 0, + const char* name = "TcpInterface") + : RNS::InterfaceImpl(name), _mode(mode), _port(port), _target_port(target_port), @@ -77,11 +78,11 @@ public: _IN = true; _OUT = true; _HW_MTU = TCP_IF_HW_MTU; - // Report low bitrate + small announce_cap so that Transport - // rate-limits announce forwarding through this interface. - // Without this the backbone floods the ESP32 with announces. - // 500 bps ≈ LoRa-class throughput; announce_cap = 2% max bandwidth. - _bitrate = 500; + // TCP links are effectively 10 Mbps+. Setting a realistic + // bitrate lets Transport prefer TCP paths over LoRa when + // both exist for the same destination. + // announce_cap = 2% keeps backbone announce flooding in check. + _bitrate = 10000000; _announce_cap = 2.0; if (target_host != nullptr) { strncpy(_target_host, target_host, sizeof(_target_host) - 1);