Root cause: Python Reticulum trims random_blobs per destination entry
(MAX_RANDOM_BLOBS=64 in-memory, PERSIST_RANDOM_BLOBS=32 on disk).
The C++ firmware had these constants defined but NEVER enforced them,
causing unbounded growth. With 21 paths x 60+ blobs x ~90 bytes each,
the destination table alone consumed ~57KB of the ESP32 324KB heap.
Fixes:
- Trim random_blobs after insert (matching Python behavior)
- Trim random_blobs on deserialization from flash
- Trim random_blobs to PERSIST_RANDOM_BLOBS on serialization
- Enforce _path_table_maxpersist when writing path table (was declared
but never used - write_path_table saved everything)
- Reduce MCU constants: MAX_RANDOM_BLOBS 64->16, PERSIST_RANDOM_BLOBS 32->8
- Reduce path_table_maxsize 128->24, maxpersist 32->12
- Add memory diagnostic after path table load
- Trim loaded paths to maxsize on startup via cull_path_table()
Results: destination_table 21KB->5.8KB, free heap 63K(22%)->156K(49%)
MODE_ACCESS_POINT unconditionally blocks announce broadcasts in
Transport::outbound(), preventing local TCP clients from discovering
each other. Changing to MODE_GATEWAY allows announces to flow through
send_outgoing() which broadcasts to all connected clients.
Root cause: receiver's announce was stored in announce_table but never
rebroadcast on LocalTcpInterface, so sender could never find a path.
Root cause: heltec_V4_boundary build was missing -DRNS_USE_TLSF=1 and
-DRNS_USE_ALLOCATOR=1 flags, causing ALL C++ new/delete to use internal
SRAM (239KB) instead of the PSRAM-backed TLSF pool (~1.6MB). Transport
data structures consumed internal heap until WiFi driver could not
allocate RX buffers (ESP_ERR_NO_MEM).
Changes:
- platformio.ini: Add TLSF/allocator flags to heltec_V4_boundary env,
re-enable NDEBUG
- Transport.cpp: Add periodic culling of _path_requests (was unbounded,
grew one entry per unique destination forever). Cull entries older than
DESTINATION_TIMEOUT. Also cull _pending_local_path_requests for removed
interfaces, and fix missing .erase() (Python .pop() equivalent).
- RNode_Firmware.ino: Replace WiFi watchdog halt-serial with auto-reboot.
Add heap pressure check (reboot if free heap < 20KB). Increase WiFi
grace period from 5s to 15s. Remove orphaned boundary_done label.
- Remove -DNDEBUG to get LOG_TRACE output for WiFi disconnect investigation
- Add WiFi watchdog in main loop: detects WiFi loss, prints diagnostics
(WiFi status, RSSI, heap, TCP state, bridge stats), then halts serial
output after 5s grace period so operator can read the last log lines
- Device keeps running as LoRa repeater even when serial is frozen
- Reboot required to resume serial output
Performance optimizations:
- Move TLSF allocator pool to PSRAM (frees ~170KB internal SRAM)
- Raise TCP_IF_MAX_CLIENTS from 4 to 8 in BOUNDARY_MODE
- Raise path_table_maxsize from 48 to 128, persist from 16 to 32
- Add -DNDEBUG to boundary build: compiles out TRACE/DEBUG macros
- Log level defaults to LOG_VERBOSE when NDEBUG defined
- Serial baud 115200 -> 921600 in BOUNDARY_MODE (reduces CPU blocking)
Previous changes included in this commit:
- Comprehensive boundary filter with transitive whitelisting (7 checks)
- destination_table erase+insert fix (std::map::insert no-overwrite bug)
- Backbone-to-backbone routing guard in next-hop forwarding
- KISS serial output disabled for boundary mode
- flash.py updates for boundary mode support
Vendor microReticulum library with boundary mode transport fixes:
- Two-whitelist system gates backbone traffic (local addresses +
mentioned addresses from local devices)
- Allow control_hashes and local destinations through boundary filter
(fixes backbone→LoRa path discovery)
- Fix get_cached_packet() to call unpack() instead of update_hash()
(fixes empty destination_hash in path responses)
- LRPROOF Identity::recall null guard
- remaining_hops HEADER_1/BROADCAST fix for final-hop delivery
- PROOF packets excluded from boundary wrapping
- Iterator invalidation fix in transport table cleanup
- is_backbone flag replaces string matching for interface identification
Firmware changes:
- Set is_backbone(true) on backbone TCP interface
- Rename default TcpInterface name to BackboneInterface
- Update comments for dual-use TcpInterface (backbone + local AP)
- Use vendored lib/microReticulum instead of PlatformIO registry
- 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.
Bridges LoRa mesh and TCP/WiFi backbone networks using microReticulum.
Based on microReticulum_Firmware with boundary mode additions:
- BoundaryMode.h: State management and EEPROM persistence
- BoundaryConfig.h: WiFi captive portal for configuration
- TcpInterface.h: TCP backbone interface with HDLC framing
- Display.h: Custom OLED layout with network status indicators
- Transport/Identity library patches for embedded memory constraints