v1.0.5: Heltec V3 support, heap stability fix, TCP reconnection improvements

- Add Heltec WiFi LoRa 32 V3 board support (8MB flash, 8MB PSRAM)
  - New heltec_V3_boundary build environment in platformio.ini
  - Board auto-detection in flash.py (8MB=V3, 16MB=V4)
  - V3 board definition in Boards.h
- Fix heap exhaustion causing watchdog reboots every ~70 min
  - Lower boundary_mentioned_addresses cap from 512 to 200
  - Heap now stable at ~38KB free (was draining to 0)
- TCP reconnection improvements in TcpInterface.h
  - SO_LINGER(0) for clean socket teardown
  - 10-minute read timeout prevents zombie connections
  - Defensive client cleanup on accept
- Add heap telemetry instrumentation (HEAP-TEL) for monitoring
- Add level guards on TRACE/DEBUG macros in Log.h
- Update README for dual V3/V4 board support
This commit is contained in:
James L
2026-02-26 14:32:18 -05:00
parent 990649d810
commit 5e0e3f538a
8 changed files with 424 additions and 111 deletions

View File

@@ -349,7 +349,11 @@
#define HAS_DISPLAY true #define HAS_DISPLAY true
#define HAS_WIFI true #define HAS_WIFI true
#define HAS_BLUETOOTH false #define HAS_BLUETOOTH false
#ifdef BOUNDARY_MODE
#define HAS_BLE false
#else
#define HAS_BLE true #define HAS_BLE true
#endif
#define HAS_PMU true #define HAS_PMU true
#define HAS_CONSOLE true #define HAS_CONSOLE true
#define HAS_EEPROM true #define HAS_EEPROM true

View File

@@ -1,6 +1,6 @@
# RNodeTHV4 — Reticulum Boundary Node for Heltec WiFi LoRa 32 V4 # RNodeTHV4 — Reticulum Boundary Node for Heltec WiFi LoRa 32 V3 / V4
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. A custom firmware for the **Heltec WiFi LoRa 32 V3** and **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 Android / Sideband Remote
@@ -28,18 +28,22 @@ Built on [microReticulum](https://github.com/attermann/microReticulum) (a C++ po
- **Optional local TCP server** — serve local devices on your WiFi in addition to the backbone connection - **Optional local TCP server** — serve local devices on your WiFi in addition to the backbone connection
- **Automatic reconnection** — WiFi and TCP connections recover from drops with exponential backoff - **Automatic reconnection** — WiFi and TCP connections recover from drops with exponential backoff
- **ESP32 memory-optimized** — table sizes, timeouts, and caching tuned for the constrained MCU environment - **ESP32 memory-optimized** — table sizes, timeouts, and caching tuned for the constrained MCU environment
- **Dual board support** — supports both Heltec V3 (8MB flash, 8MB PSRAM) and V4 (16MB flash, 2MB PSRAM) with automatic board detection
## Hardware ## Hardware
The **Heltec WiFi LoRa 32 V4** was chosen because it ships standard with **2 MB PSRAM** and **16 MB flash** — enough headroom for the microReticulum transport tables, packet caching to flash storage, and the web-based configuration portal. Many other LoRa dev boards come with only 48 MB flash and no PSRAM, which would require significant compromises to the boundary node's caching and routing capabilities. Both the **Heltec WiFi LoRa 32 V3** and **V4** are supported. These boards were chosen because they ship with PSRAM and ample flash — enough headroom for the microReticulum transport tables, packet caching to flash storage, and the web-based configuration portal. Many other LoRa dev boards come with only 4 MB flash and no PSRAM, which would require significant compromises to the boundary node's caching and routing capabilities.
| Component | Spec | | Component | Heltec V3 | Heltec V4 |
|-----------|------| |-----------|-----------|----------|
| **Board** | Heltec WiFi LoRa 32 V4 | | **MCU** | ESP32-S3 | ESP32-S3 |
| **MCU** | ESP32-S3, 2MB PSRAM, 16MB Flash | | **Flash** | 8 MB | 16 MB |
| **Radio** | SX1262 + GC1109 PA (up to 28 dBm) | | **PSRAM** | 8 MB (QSPI) | 2 MB (QSPI) |
| **Display** | SSD1306 OLED 128×64 | | **Radio** | SX1262 | SX1262 + GC1109 PA |
| **WiFi** | 2.4 GHz 802.11 b/g/n | | **TX Power** | Up to 22 dBm | Up to 28 dBm |
| **Display** | SSD1306 OLED 128×64 | SSD1306 OLED 128×64 |
| **WiFi** | 2.4 GHz 802.11 b/g/n | 2.4 GHz 802.11 b/g/n |
| **USB** | Native USB CDC | Native USB CDC |
## Quick Start ## Quick Start
@@ -56,13 +60,18 @@ git clone https://github.com/jrl290/RNodeTHV4.git
cd RNodeTHV4 cd RNodeTHV4
# Download latest firmware from GitHub Releases and flash # Download latest firmware from GitHub Releases and flash
# (auto-detects V3 vs V4 from flash size)
python flash.py --download python flash.py --download
# Or specify board explicitly
python flash.py --download --board v3
python flash.py --download --board v4
# Or flash a local binary # Or flash a local binary
python flash.py --file rnodethv4_firmware.bin python flash.py --file rnodethv4_firmware.bin
``` ```
The flash utility will list all available serial ports and prompt you to choose one. If no ports are detected, you may need to hold the **BOOT** button while pressing **RESET** to enter download mode. The flash utility auto-detects whether a V3 or V4 is connected by querying the flash size (8MB = V3, 16MB = V4). You can override with `--board v3` or `--board v4`. It will list all available serial ports and prompt you to choose one. If no ports are detected, you may need to hold the **BOOT** button while pressing **RESET** to enter download mode.
### Option B: Build from Source (PlatformIO) ### Option B: Build from Source (PlatformIO)
@@ -74,15 +83,18 @@ For development or customization:
git clone https://github.com/jrl290/RNodeTHV4.git git clone https://github.com/jrl290/RNodeTHV4.git
cd RNodeTHV4 cd RNodeTHV4
# Build # Build for V4
pio run -e heltec_V4_boundary pio run -e heltec_V4_boundary
# Build for V3
pio run -e heltec_V3_boundary
# Flash (via PlatformIO) # Flash (via PlatformIO)
pio run -e heltec_V4_boundary -t upload pio run -e heltec_V4_boundary -t upload
# Or create a merged binary and flash with the utility # Or create a merged binary and flash with the utility
python flash.py --merge-only # creates rnodethv4_firmware.bin python flash.py --merge-only # creates merged firmware bin
python flash.py # flash it python flash.py # flash it (auto-detects board)
# Monitor serial output (optional) # Monitor serial output (optional)
pio device monitor -e heltec_V4_boundary pio device monitor -e heltec_V4_boundary
@@ -326,8 +338,8 @@ Set the boundary node's **Local TCP Server** to **Enabled** (port 4242).
| `TcpInterface.h` | TCP interface for both backbone and local server (implements `RNS::InterfaceImpl`) with HDLC framing, unique naming, and 10 Mbps bitrate | | `TcpInterface.h` | TCP interface for both backbone and local server (implements `RNS::InterfaceImpl`) with HDLC framing, unique naming, and 10 Mbps bitrate |
| `Display.h` | OLED display layout — boundary-specific status page | | `Display.h` | OLED display layout — boundary-specific status page |
| `flash.py` | Flash utility — list serial ports, download from GitHub, merge & flash firmware | | `flash.py` | Flash utility — list serial ports, download from GitHub, merge & flash firmware |
| `Boards.h` | Board variant definition for `heltec32v4_boundary` | | `Boards.h` | Board variant definitions for V3 and V4 |
| `platformio.ini` | Build targets: `heltec_V4_boundary` and `heltec_V4_boundary-local` | | `platformio.ini` | Build targets: `heltec_V3_boundary`, `heltec_V4_boundary`, and `heltec_V4_boundary-local` |
### Library Patches ### Library Patches
@@ -340,12 +352,12 @@ The firmware depends on [microReticulum](https://github.com/attermann/microRetic
| `Identity.cpp` | `_known_destinations_maxsize` = 24, `cull_known_destinations()` | | `Identity.cpp` | `_known_destinations_maxsize` = 24, `cull_known_destinations()` |
| `Type.h` | `MODE_BOUNDARY` = 0x20, reduced `MAX_QUEUED_ANNOUNCES`, `MAX_RECEIPTS`, shorter timeouts | | `Type.h` | `MODE_BOUNDARY` = 0x20, reduced `MAX_QUEUED_ANNOUNCES`, `MAX_RECEIPTS`, shorter timeouts |
### Memory Usage (typical) ### Memory Usage (typical, V4)
| Resource | Used | Available | | Resource | Used | Available |
|----------|------|-----------| |----------|------|----------|
| RAM | ~21.7% | 320 KB | | RAM | ~21.7% | 320 KB |
| Flash | ~18.1% | 16 MB | | Flash | ~18.4% | 16 MB |
| PSRAM | Dynamic | 2 MB | | PSRAM | Dynamic | 2 MB |
## License ## License

View File

@@ -635,7 +635,8 @@ void setup() {
RNS::Transport::set_transmit_packet_callback(on_transmit_packet); RNS::Transport::set_transmit_packet_callback(on_transmit_packet);
Serial.write("Starting RNS...\r\n"); Serial.write("Starting RNS...\r\n");
RNS::loglevel(RNS::LOG_TRACE); RNS::loglevel(RNS::LOG_VERBOSE);
//RNS::loglevel(RNS::LOG_TRACE);
//RNS::loglevel(RNS::LOG_MEM); //RNS::loglevel(RNS::LOG_MEM);
HEAD("Registering LoRA Interface...", RNS::LOG_TRACE); HEAD("Registering LoRA Interface...", RNS::LOG_TRACE);
@@ -701,6 +702,9 @@ void setup() {
0, 0,
"LocalTcpInterface" "LocalTcpInterface"
); );
// rnsd can be quiet for long stretches — use 10 min timeout
// to prevent unnecessary reconnection cycles that leak lwIP memory
local_tcp_interface_ptr->setReadTimeout(600000);
local_tcp_rns_interface = local_tcp_interface_ptr; local_tcp_rns_interface = local_tcp_interface_ptr;
local_tcp_rns_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT); local_tcp_rns_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT);
RNS::Transport::register_interface(local_tcp_rns_interface); RNS::Transport::register_interface(local_tcp_rns_interface);

View File

@@ -17,6 +17,7 @@
#ifdef BOUNDARY_MODE #ifdef BOUNDARY_MODE
#include <WiFi.h> #include <WiFi.h>
#include <lwip/sockets.h> // SO_LINGER — force RST to free lwIP PCBs immediately
#include <Interface.h> #include <Interface.h>
#include <Transport.h> #include <Transport.h>
#include <Bytes.h> #include <Bytes.h>
@@ -75,6 +76,7 @@ public:
_last_reconnect(0), _last_reconnect(0),
_last_keepalive(0), _last_keepalive(0),
_reconnect_interval(TCP_IF_RECONNECT_MIN), _reconnect_interval(TCP_IF_RECONNECT_MIN),
_read_timeout(TCP_IF_READ_TIMEOUT),
_resolved_ip((uint32_t)0), _resolved_ip((uint32_t)0),
_consecutive_failures(0), _consecutive_failures(0),
_started(false) _started(false)
@@ -128,7 +130,16 @@ public:
void stop() { void stop() {
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
if (_clients[i].active) { if (_clients[i].active) {
// Force RST to free lwIP PCBs immediately (no TIME_WAIT)
int fd = _clients[i].client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
}
_clients[i].client.stop(); _clients[i].client.stop();
_clients[i].client = WiFiClient();
_clients[i].active = false; _clients[i].active = false;
} }
} }
@@ -185,25 +196,15 @@ public:
if (!_clients[i].active) continue; if (!_clients[i].active) continue;
if (!_clients[i].client.connected()) { if (!_clients[i].client.connected()) {
Serial.printf("[TcpIF] Client %d disconnected\r\n", i); _cleanup_client(i, "disconnected");
_clients[i].client.stop();
_clients[i].active = false;
_clients[i].in_frame = false;
_clients[i].escape = false;
_clients[i].rxlen = 0;
_num_clients--;
continue; continue;
} }
// Check read timeout // Check read timeout (0 = disabled)
if (_clients[i].last_activity > 0 && if (_read_timeout > 0 &&
(millis() - _clients[i].last_activity) > TCP_IF_READ_TIMEOUT) { _clients[i].last_activity > 0 &&
Serial.printf("[TcpIF] Client %d read timeout\r\n", i); (millis() - _clients[i].last_activity) > _read_timeout) {
_clients[i].client.stop(); _cleanup_client(i, "read timeout");
_clients[i].active = false;
_clients[i].in_frame = false;
_clients[i].rxlen = 0;
_num_clients--;
continue; continue;
} }
@@ -220,6 +221,7 @@ public:
int clientCount() const { return _num_clients; } int clientCount() const { return _num_clients; }
bool isStarted() const { return _started; } bool isStarted() const { return _started; }
bool isConnected() const { return _num_clients > 0; } bool isConnected() const { return _num_clients > 0; }
void setReadTimeout(uint32_t timeout_ms) { _read_timeout = timeout_ms; }
protected: protected:
// ─── RNS InterfaceImpl: outgoing packet from RNS Transport ─────────────── // ─── RNS InterfaceImpl: outgoing packet from RNS Transport ───────────────
@@ -248,12 +250,7 @@ protected:
if (_clients[i].active && _clients[i].client.connected()) { if (_clients[i].active && _clients[i].client.connected()) {
size_t written = _clients[i].client.write(frame_buf, flen); size_t written = _clients[i].client.write(frame_buf, flen);
if (written == 0) { if (written == 0) {
Serial.printf("[TcpIF] Write failed on client %d, dropping\r\n", i); _cleanup_client(i, "write failed");
_clients[i].client.stop();
_clients[i].active = false;
_clients[i].in_frame = false;
_clients[i].rxlen = 0;
_num_clients--;
} }
} }
} }
@@ -270,6 +267,38 @@ protected:
} }
private: private:
// ─── Cleanup a client slot, freeing all lwIP resources ───────────────────
void _cleanup_client(int idx, const char* reason) {
TcpClient& c = _clients[idx];
if (!c.active) return;
uint32_t heap_before = ESP.getFreeHeap();
// Set SO_LINGER with timeout 0: forces RST instead of FIN,
// which skips TIME_WAIT and immediately frees the lwIP PCB
// and all associated TCP send/receive buffers (~2-4 KB each).
int fd = c.client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
}
c.client.stop();
c.client = WiFiClient(); // Release any residual shared_ptr state
c.active = false;
c.in_frame = false;
c.escape = false;
c.rxlen = 0;
_num_clients--;
uint32_t heap_after = ESP.getFreeHeap();
Serial.printf("[TcpIF] Client %d %s (heap: %u -> %u, delta: %+d)\r\n",
idx, reason, heap_before, heap_after,
(int)(heap_after - heap_before));
}
// ─── HDLC byte-level deframing ────────────────────────────────────────── // ─── HDLC byte-level deframing ──────────────────────────────────────────
void _hdlc_deframe(int idx, uint8_t byte) { void _hdlc_deframe(int idx, uint8_t byte) {
TcpClient& c = _clients[idx]; TcpClient& c = _clients[idx];
@@ -306,6 +335,18 @@ private:
// Find a free slot // Find a free slot
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
if (!_clients[i].active) { if (!_clients[i].active) {
// Defensive: force-release any residual lwIP resources in this slot
// before assigning the new client (prevents PCB/buffer leaks)
int fd = _clients[i].client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
_clients[i].client.stop();
}
_clients[i].client = WiFiClient(); // Reset to clean state
_clients[i].client = newClient; _clients[i].client = newClient;
_clients[i].client.setNoDelay(true); _clients[i].client.setNoDelay(true);
_clients[i].client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000); _clients[i].client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000);
@@ -399,6 +440,7 @@ private:
uint32_t _last_reconnect; uint32_t _last_reconnect;
uint32_t _last_keepalive; uint32_t _last_keepalive;
uint32_t _reconnect_interval; uint32_t _reconnect_interval;
uint32_t _read_timeout;
IPAddress _resolved_ip; IPAddress _resolved_ip;
uint16_t _consecutive_failures; uint16_t _consecutive_failures;
bool _started; bool _started;

294
flash.py
View File

@@ -2,16 +2,19 @@
""" """
RNodeTHV4 Flash Utility RNodeTHV4 Flash Utility
Flash the RNodeTHV4 boundary node firmware to a Heltec WiFi LoRa 32 V4. Flash the RNodeTHV4 boundary node firmware to a Heltec WiFi LoRa 32 V3 or V4.
No PlatformIO required — just Python 3 and a USB cable. No PlatformIO required — just Python 3 and a USB cable.
Default mode flashes only the app partition (0x10000), preserving Default mode flashes only the app partition (0x10000), preserving
bootloader, partition table, NVS, and EEPROM settings. bootloader, partition table, NVS, and EEPROM settings.
Usage: Usage:
# Update firmware (preserves WiFi/boundary settings) # Update firmware — V4 (default)
python flash.py python flash.py
# Update firmware — V3
python flash.py --board v3
# Full flash with merged binary (overwrites everything) # Full flash with merged binary (overwrites everything)
python flash.py --full python flash.py --full
@@ -43,9 +46,6 @@ import time
CHIP = "esp32s3" CHIP = "esp32s3"
FLASH_MODE = "qio" FLASH_MODE = "qio"
FLASH_FREQ = "80m" FLASH_FREQ = "80m"
FLASH_SIZE = "16MB"
BAUD_RATE = "921600"
MERGED_FILENAME = "rnodethv4_firmware.bin"
GITHUB_REPO = "jrl290/RNodeTHV4" GITHUB_REPO = "jrl290/RNodeTHV4"
# Flash addresses for ESP32-S3 Arduino framework # Flash addresses for ESP32-S3 Arduino framework
@@ -54,11 +54,61 @@ PARTITIONS_ADDR = 0x8000
BOOT_APP0_ADDR = 0xe000 BOOT_APP0_ADDR = 0xe000
APP_ADDR = 0x10000 APP_ADDR = 0x10000
# PlatformIO build output paths (relative to project root) # ── Board profiles ─────────────────────────────────────────────────────────────
BUILD_DIR = ".pio/build/heltec_V4_boundary" # Each board defines its PIO env, flash size, baud rate, firmware binary name,
BOOTLOADER_BIN = os.path.join(BUILD_DIR, "bootloader.bin") # and merged binary name.
PARTITIONS_BIN = os.path.join(BUILD_DIR, "partitions.bin")
FIRMWARE_BIN = os.path.join(BUILD_DIR, "rnode_firmware_heltec32v4_boundary.bin") BOARD_PROFILES = {
"v4": {
"name": "Heltec WiFi LoRa 32 V4",
"pio_env": "heltec_V4_boundary",
"build_dir": ".pio/build/heltec_V4_boundary",
"firmware_bin": "rnode_firmware_heltec32v4_boundary.bin",
"merged_filename": "rnodethv4_firmware.bin",
"flash_size": "16MB",
"baud_rate": "921600",
},
"v3": {
"name": "Heltec WiFi LoRa 32 V3",
"pio_env": "heltec_V3_boundary",
"build_dir": ".pio/build/heltec_V3_boundary",
"firmware_bin": "rnode_firmware_heltec32v3.bin",
"merged_filename": "rnodethv3_firmware.bin",
"flash_size": "8MB",
"baud_rate": "460800",
},
}
DEFAULT_BOARD = "v4"
# Active board profile (set in main() from --board arg)
_board = None
def board_profile():
return BOARD_PROFILES[_board or DEFAULT_BOARD]
def BUILD_DIR():
return board_profile()["build_dir"]
def BOOTLOADER_BIN():
return os.path.join(BUILD_DIR(), "bootloader.bin")
def PARTITIONS_BIN():
return os.path.join(BUILD_DIR(), "partitions.bin")
def FIRMWARE_BIN():
return os.path.join(BUILD_DIR(), board_profile()["firmware_bin"])
def FLASH_SIZE():
return board_profile()["flash_size"]
def BAUD_RATE():
return board_profile()["baud_rate"]
def MERGED_FILENAME():
return board_profile()["merged_filename"]
def PIO_ENV():
return board_profile()["pio_env"]
# ESP32 partition table magic bytes (first two bytes of a partition table entry) # ESP32 partition table magic bytes (first two bytes of a partition table entry)
PARTITION_TABLE_MAGIC = b'\xaa\x50' PARTITION_TABLE_MAGIC = b'\xaa\x50'
@@ -94,6 +144,16 @@ def _find_in_platformio_or_release(build_path, release_name):
return None return None
# Forward-compatible aliases (these are now functions, not constants)
def _bootloader_bin():
return BOOTLOADER_BIN()
def _partitions_bin():
return PARTITIONS_BIN()
def _firmware_bin():
return FIRMWARE_BIN()
def find_boot_app0(): def find_boot_app0():
"""Find boot_app0.bin from PlatformIO framework packages. """Find boot_app0.bin from PlatformIO framework packages.
@@ -126,16 +186,77 @@ def find_boot_app0():
def find_bootloader(): def find_bootloader():
"""Find bootloader.bin from PlatformIO build output or Release/ bundle.""" """Find bootloader.bin from PlatformIO build output or Release/ bundle."""
return _find_in_platformio_or_release(BOOTLOADER_BIN, "bootloader.bin") return _find_in_platformio_or_release(BOOTLOADER_BIN(), "bootloader.bin")
def find_partitions(): def find_partitions():
"""Find partitions.bin from PlatformIO build output or Release/ bundle.""" """Find partitions.bin from PlatformIO build output or Release/ bundle."""
return _find_in_platformio_or_release(PARTITIONS_BIN, "partitions.bin") return _find_in_platformio_or_release(PARTITIONS_BIN(), "partitions.bin")
BOOT_APP0_BIN = find_boot_app0() BOOT_APP0_BIN = find_boot_app0()
# ── Board auto-detection ───────────────────────────────────────────────────────
# Map detected flash sizes to board keys
_FLASH_SIZE_TO_BOARD = {
"16MB": "v4",
"8MB": "v3",
}
def detect_board(port, esptool_cmd):
"""Auto-detect which Heltec board is connected by querying flash size.
Runs ``esptool.py flash_id`` and parses the output for:
- Detected flash size (16MB → V4, 8MB → V3)
- Chip type (ESP32-S3 expected)
- Features (PSRAM size, WiFi, BLE)
Returns a tuple (board_key, info_dict) on success, or (None, reason) on
failure. ``board_key`` is "v3" or "v4".
"""
cmd = esptool_cmd + ["--port", port, "flash_id"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
except subprocess.TimeoutExpired:
return None, "esptool timed out (device not responding?)"
except Exception as e:
return None, str(e)
output = result.stdout + result.stderr
if result.returncode != 0:
return None, f"esptool flash_id failed:\n{output.strip()}"
# Parse key fields
info = {}
for line in output.splitlines():
line = line.strip()
if line.startswith("Chip is "):
info["chip"] = line[len("Chip is "):]
elif line.startswith("Features:"):
info["features"] = line[len("Features:"):].strip()
elif line.startswith("Detected flash size:"):
info["flash_size"] = line.split(":")[-1].strip()
elif line.startswith("MAC:"):
info["mac"] = line.split(":")[-5:] # last 5 colon-groups
info["mac"] = line[len("MAC:"):].strip()
elif line.startswith("Crystal is"):
info["crystal"] = line[len("Crystal is"):].strip()
flash_size = info.get("flash_size")
if not flash_size:
return None, f"Could not parse flash size from esptool output:\n{output.strip()}"
board_key = _FLASH_SIZE_TO_BOARD.get(flash_size)
if not board_key:
return None, (
f"Unknown flash size '{flash_size}' — expected 16MB (V4) or 8MB (V3).\n"
f"Use --board v3 or --board v4 to specify manually."
)
return board_key, info
# ── Helpers ──────────────────────────────────────────────────────────────────── # ── Helpers ────────────────────────────────────────────────────────────────────
def find_esptool(): def find_esptool():
@@ -259,16 +380,16 @@ def download_firmware(dest_path):
# Find the merged firmware asset # Find the merged firmware asset
asset_url = None asset_url = None
for asset in release.get("assets", []): for asset in release.get("assets", []):
if asset["name"] == MERGED_FILENAME: if asset["name"] == MERGED_FILENAME():
asset_url = asset["browser_download_url"] asset_url = asset["browser_download_url"]
break break
if not asset_url: if not asset_url:
print(f"Error: '{MERGED_FILENAME}' not found in latest release ({release.get('tag_name', '?')}).") print(f"Error: '{MERGED_FILENAME()}' not found in latest release ({release.get('tag_name', '?')}).")
print("Available assets:", [a["name"] for a in release.get("assets", [])]) print("Available assets:", [a["name"] for a in release.get("assets", [])])
return False return False
print(f"Downloading {release['tag_name']} / {MERGED_FILENAME}...") print(f"Downloading {release['tag_name']} / {MERGED_FILENAME()}...")
try: try:
urlretrieve(asset_url, dest_path) urlretrieve(asset_url, dest_path)
except Exception as e: except Exception as e:
@@ -293,7 +414,7 @@ def _do_merge(output_path, esptool_cmd, bootloader, partitions, boot_app0, firmw
"merge_bin", "merge_bin",
"--flash_mode", FLASH_MODE, "--flash_mode", FLASH_MODE,
"--flash_freq", FLASH_FREQ, "--flash_freq", FLASH_FREQ,
"--flash_size", FLASH_SIZE, "--flash_size", FLASH_SIZE(),
"-o", output_path, "-o", output_path,
f"0x{BOOTLOADER_ADDR:x}", bootloader, f"0x{BOOTLOADER_ADDR:x}", bootloader,
f"0x{PARTITIONS_ADDR:x}", partitions, f"0x{PARTITIONS_ADDR:x}", partitions,
@@ -321,11 +442,11 @@ def merge_firmware(output_path, esptool_cmd):
bootloader = find_bootloader() bootloader = find_bootloader()
partitions = find_partitions() partitions = find_partitions()
boot_app0 = BOOT_APP0_BIN boot_app0 = BOOT_APP0_BIN
firmware = FIRMWARE_BIN firmware = FIRMWARE_BIN()
missing = [] missing = []
if not bootloader: missing.append(("bootloader", BOOTLOADER_BIN)) if not bootloader: missing.append(("bootloader", BOOTLOADER_BIN()))
if not partitions: missing.append(("partitions", PARTITIONS_BIN)) if not partitions: missing.append(("partitions", PARTITIONS_BIN()))
if not boot_app0: missing.append(("boot_app0", "(not found)")) if not boot_app0: missing.append(("boot_app0", "(not found)"))
if not os.path.isfile(firmware): if not os.path.isfile(firmware):
missing.append(("firmware", firmware)) missing.append(("firmware", firmware))
@@ -333,7 +454,7 @@ def merge_firmware(output_path, esptool_cmd):
if missing: if missing:
for name, path in missing: for name, path in missing:
print(f"Error: {name} not found: {path}") print(f"Error: {name} not found: {path}")
print("Run 'pio run -e heltec_V4_boundary' to build first.") print(f"Run 'pio run -e {PIO_ENV()}' to build first.")
return False return False
return _do_merge(output_path, esptool_cmd, bootloader, partitions, boot_app0, firmware) return _do_merge(output_path, esptool_cmd, bootloader, partitions, boot_app0, firmware)
@@ -360,7 +481,7 @@ def auto_merge_app_binary(app_binary_path, esptool_cmd):
if missing: if missing:
print(f"Cannot auto-merge: missing {', '.join(missing)}") print(f"Cannot auto-merge: missing {', '.join(missing)}")
print("Place them in the Release/ folder alongside flash.py, or") print("Place them in the Release/ folder alongside flash.py, or")
print("build with PlatformIO: pio run -e heltec_V4_boundary") print(f"build with PlatformIO: pio run -e {PIO_ENV()}")
return None return None
# Create merged binary next to the app binary # Create merged binary next to the app binary
@@ -406,10 +527,13 @@ def reset_to_bootloader(port):
return True return True
def flash_firmware(firmware_path, port, esptool_cmd, baud=BAUD_RATE): def flash_firmware(firmware_path, port, esptool_cmd, baud=None):
"""Flash firmware to the device.""" """Flash firmware to the device."""
if baud is None:
baud = BAUD_RATE()
flash_size = FLASH_SIZE()
print(f"\nFlashing {firmware_path} to {port}...") print(f"\nFlashing {firmware_path} to {port}...")
print(f" Chip: {CHIP} Baud: {baud} Flash: {FLASH_SIZE}\n") print(f" Chip: {CHIP} Baud: {baud} Flash: {flash_size}\n")
# Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000) # Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000)
is_merged = is_merged_binary(firmware_path) is_merged = is_merged_binary(firmware_path)
@@ -431,7 +555,7 @@ def flash_firmware(firmware_path, port, esptool_cmd, baud=BAUD_RATE):
"-z", "-z",
"--flash_mode", FLASH_MODE, "--flash_mode", FLASH_MODE,
"--flash_freq", FLASH_FREQ, "--flash_freq", FLASH_FREQ,
"--flash_size", FLASH_SIZE, "--flash_size", flash_size,
flash_addr, firmware_path, flash_addr, firmware_path,
] ]
@@ -443,12 +567,14 @@ def flash_firmware(firmware_path, port, esptool_cmd, baud=BAUD_RATE):
# ── Main ─────────────────────────────────────────────────────────────────────── # ── Main ───────────────────────────────────────────────────────────────────────
def main(): def main():
global _board
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="RNodeTHV4 Flash Utility — flash boundary node firmware to Heltec V4", description="RNodeTHV4 Flash Utility — flash boundary node firmware to Heltec V3/V4",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
python flash.py # App-only update (preserves settings) python flash.py # App-only update, V4 (default)
python flash.py --board v3 # App-only update, V3
python flash.py --full # Full flash with merged binary python flash.py --full # Full flash with merged binary
python flash.py --download # Download latest release and flash python flash.py --download # Download latest release and flash
python flash.py --file firmware.bin # Flash a specific file python flash.py --file firmware.bin # Flash a specific file
@@ -457,9 +583,12 @@ Examples:
python flash.py --erase --full # Erase flash, then full flash python flash.py --erase --full # Erase flash, then full flash
""", """,
) )
parser.add_argument("--board", choices=["v3", "v4"], default=None,
help="Target board: v3 (Heltec V3) or v4 (Heltec V4). "
"Auto-detected from connected device if omitted.")
parser.add_argument("--file", "-f", help="Path to firmware binary to flash") parser.add_argument("--file", "-f", help="Path to firmware binary to flash")
parser.add_argument("--port", "-p", help="Serial port (auto-detected if omitted)") parser.add_argument("--port", "-p", help="Serial port (auto-detected if omitted)")
parser.add_argument("--baud", "-b", default=BAUD_RATE, help=f"Baud rate (default: {BAUD_RATE})") parser.add_argument("--baud", "-b", default=None, help="Baud rate (board-specific default)")
parser.add_argument("--download", "-d", action="store_true", parser.add_argument("--download", "-d", action="store_true",
help="Download latest firmware from GitHub Releases") help="Download latest firmware from GitHub Releases")
parser.add_argument("--merge-only", action="store_true", parser.add_argument("--merge-only", action="store_true",
@@ -470,20 +599,58 @@ Examples:
help="Erase entire flash before writing (implies --full)") help="Erase entire flash before writing (implies --full)")
args = parser.parse_args() args = parser.parse_args()
baud = args.baud
print("╔══════════════════════════════════════════╗") # Find esptool early — needed for both auto-detect and flashing
print("║ RNodeTHV4 Flash Utility ║")
print("║ Heltec WiFi LoRa 32 V4 Boundary Node ║")
print("╚══════════════════════════════════════════╝")
print()
# Find esptool
esptool_cmd = find_esptool() esptool_cmd = find_esptool()
if not esptool_cmd: if not esptool_cmd:
print("Error: esptool not found!") print("Error: esptool not found!")
print("Install it with: pip install esptool") print("Install it with: pip install esptool")
sys.exit(1) sys.exit(1)
# ── Board detection ─────────────────────────────────────────────────
detected_info = None
_early_port = None
if args.board:
# Explicit board — no detection needed
_board = args.board
elif args.merge_only:
# No device needed for merge — fall back to default
_board = DEFAULT_BOARD
print(f"(No --board specified; defaulting to {DEFAULT_BOARD} for merge)")
else:
# Auto-detect from connected device
_early_port = args.port or find_serial_port()
if not _early_port:
print("No serial port detected and no --board specified.")
print(f"Defaulting to {DEFAULT_BOARD}. Specify with --board v3 or --board v4.")
_board = DEFAULT_BOARD
else:
print(f"Detecting board on {_early_port}...")
board_key, info = detect_board(_early_port, esptool_cmd)
if board_key:
_board = board_key
detected_info = info
print(f" Chip: {info.get('chip', '?')}")
print(f" Flash: {info.get('flash_size', '?')}")
print(f" Features: {info.get('features', '?')}")
print(f" MAC: {info.get('mac', '?')}")
print(f" → Detected: {BOARD_PROFILES[board_key]['name']}")
else:
reason = info # info is the error reason when board_key is None
print(f" Auto-detect failed: {reason}")
print(f" Defaulting to {DEFAULT_BOARD}. Specify with --board v3 or --board v4.")
_board = DEFAULT_BOARD
baud = args.baud or BAUD_RATE()
bp = board_profile()
print()
print("╔══════════════════════════════════════════╗")
print("║ RNodeTHV4 Flash Utility ║")
print(f"{bp['name']:^40s}")
print("╚══════════════════════════════════════════╝")
print()
print(f"Using esptool: {' '.join(esptool_cmd)}") print(f"Using esptool: {' '.join(esptool_cmd)}")
# --erase implies --full (after erase, device needs bootloader + partitions) # --erase implies --full (after erase, device needs bootloader + partitions)
@@ -492,6 +659,9 @@ Examples:
# Determine firmware file # Determine firmware file
firmware_path = None firmware_path = None
merged_fn = MERGED_FILENAME()
firmware_bin = FIRMWARE_BIN()
pio_env = PIO_ENV()
if args.file: if args.file:
firmware_path = args.file firmware_path = args.file
@@ -500,69 +670,69 @@ Examples:
sys.exit(1) sys.exit(1)
elif args.download: elif args.download:
firmware_path = MERGED_FILENAME firmware_path = merged_fn
if not download_firmware(firmware_path): if not download_firmware(firmware_path):
sys.exit(1) sys.exit(1)
elif args.merge_only: elif args.merge_only:
if merge_firmware(MERGED_FILENAME, esptool_cmd): if merge_firmware(merged_fn, esptool_cmd):
print(f"\nDone! Flash with: python flash.py --file {MERGED_FILENAME}") print(f"\nDone! Flash with: python flash.py --board {_board} --file {merged_fn}")
else: else:
sys.exit(1) sys.exit(1)
return return
elif args.full: elif args.full:
# Full flash: use or create merged binary # Full flash: use or create merged binary
if os.path.isfile(FIRMWARE_BIN): if os.path.isfile(firmware_bin):
# Build exists — (re-)merge # Build exists — (re-)merge
if os.path.isfile(MERGED_FILENAME): if os.path.isfile(merged_fn):
build_time = os.path.getmtime(FIRMWARE_BIN) build_time = os.path.getmtime(firmware_bin)
merge_time = os.path.getmtime(MERGED_FILENAME) merge_time = os.path.getmtime(merged_fn)
if build_time > merge_time: if build_time > merge_time:
print("Build output is newer than merged binary, re-merging...") print("Build output is newer than merged binary, re-merging...")
if not merge_firmware(MERGED_FILENAME, esptool_cmd): if not merge_firmware(merged_fn, esptool_cmd):
sys.exit(1) sys.exit(1)
else: else:
print("Creating merged binary from PlatformIO build output...") print("Creating merged binary from PlatformIO build output...")
if not merge_firmware(MERGED_FILENAME, esptool_cmd): if not merge_firmware(merged_fn, esptool_cmd):
sys.exit(1) sys.exit(1)
firmware_path = MERGED_FILENAME firmware_path = merged_fn
elif os.path.isfile(MERGED_FILENAME): elif os.path.isfile(merged_fn):
firmware_path = MERGED_FILENAME firmware_path = merged_fn
else: else:
print("No firmware found for full flash!") print("No firmware found for full flash!")
print() print()
print("Options:") print("Options:")
print(" 1. Build with PlatformIO first: pio run -e heltec_V4_boundary") print(f" 1. Build with PlatformIO first: pio run -e {pio_env}")
print(" 2. Download from GitHub: python flash.py --download") print(f" 2. Download from GitHub: python flash.py --board {_board} --download")
print(" 3. Specify a file: python flash.py --file <path>") print(f" 3. Specify a file: python flash.py --board {_board} --file <path>")
sys.exit(1) sys.exit(1)
else: else:
# Default: app-only flash (preserves settings) # Default: app-only flash (preserves settings)
if os.path.isfile(FIRMWARE_BIN): if os.path.isfile(firmware_bin):
firmware_path = FIRMWARE_BIN firmware_path = firmware_bin
print(f"App-only update (preserves WiFi/boundary settings)") print(f"App-only update (preserves WiFi/boundary settings)")
print(f" Use --full for a complete flash, or --erase for recovery.") print(f" Use --full for a complete flash, or --erase for recovery.")
elif os.path.isfile(MERGED_FILENAME): elif os.path.isfile(merged_fn):
firmware_path = MERGED_FILENAME firmware_path = merged_fn
print(f"No build output found, using merged binary: {MERGED_FILENAME}") print(f"No build output found, using merged binary: {merged_fn}")
print(f" Note: merged binary will overwrite bootloader + partitions.") print(f" Note: merged binary will overwrite bootloader + partitions.")
else: else:
print("No firmware found!") print("No firmware found!")
print() print()
print("Options:") print("Options:")
print(" 1. Build with PlatformIO first: pio run -e heltec_V4_boundary") print(f" 1. Build with PlatformIO first: pio run -e {pio_env}")
print(" 2. Download from GitHub: python flash.py --download") print(f" 2. Download from GitHub: python flash.py --board {_board} --download")
print(" 3. Specify a file: python flash.py --file <path>") print(f" 3. Specify a file: python flash.py --board {_board} --file <path>")
sys.exit(1) sys.exit(1)
# Flash # Flash — reuse early-detected port if available
port = args.port or find_serial_port() port = args.port or _early_port or find_serial_port()
if not port: if not port:
print("\nError: No serial port detected!") print("\nError: No serial port detected!")
print("Connect your Heltec V4 via USB and try again,") print(f"Connect your {bp['name']} via USB and try again,")
print("or specify manually: python flash.py --port /dev/ttyACM0") print(f"or specify manually: python flash.py --board {_board} --port /dev/ttyACM0")
sys.exit(1) sys.exit(1)
print(f"\nSerial port: {port}") print(f"\nSerial port: {port}")

View File

@@ -25,10 +25,10 @@
#define VERBOSE(msg) (RNS::verbose(msg)) #define VERBOSE(msg) (RNS::verbose(msg))
#define VERBOSEF(msg, ...) (RNS::verbosef(msg, __VA_ARGS__)) #define VERBOSEF(msg, ...) (RNS::verbosef(msg, __VA_ARGS__))
#ifndef NDEBUG #ifndef NDEBUG
#define DEBUG(msg) (RNS::debug(msg)) #define DEBUG(msg) do { if (RNS::loglevel() >= RNS::LOG_DEBUG) RNS::debug(msg); } while(0)
#define DEBUGF(msg, ...) (RNS::debugf(msg, __VA_ARGS__)) #define DEBUGF(msg, ...) do { if (RNS::loglevel() >= RNS::LOG_DEBUG) RNS::debugf(msg, __VA_ARGS__); } while(0)
#define TRACE(msg) (RNS::trace(msg)) #define TRACE(msg) do { if (RNS::loglevel() >= RNS::LOG_TRACE) RNS::trace(msg); } while(0)
#define TRACEF(msg, ...) (RNS::tracef(msg, __VA_ARGS__)) #define TRACEF(msg, ...) do { if (RNS::loglevel() >= RNS::LOG_TRACE) RNS::tracef(msg, __VA_ARGS__); } while(0)
#if defined(RNS_MEM_LOG) #if defined(RNS_MEM_LOG)
#define MEM(msg) (RNS::mem(msg)) #define MEM(msg) (RNS::mem(msg))
#define MEMF(msg, ...) (RNS::memf(msg, __VA_ARGS__)) #define MEMF(msg, ...) (RNS::memf(msg, __VA_ARGS__))

View File

@@ -104,6 +104,7 @@ using namespace RNS::Utilities;
static std::set<Bytes> _boundary_local_addresses; static std::set<Bytes> _boundary_local_addresses;
// BOUNDARY MODE Whitelist 2: addresses mentioned in packets from local devices // BOUNDARY MODE Whitelist 2: addresses mentioned in packets from local devices
static std::set<Bytes> _boundary_mentioned_addresses; static std::set<Bytes> _boundary_mentioned_addresses;
static const uint16_t _boundary_maxsize = 200;
// BOUNDARY MODE: Check if an interface is the backbone // BOUNDARY MODE: Check if an interface is the backbone
static bool is_backbone_interface(const Interface& iface) { static bool is_backbone_interface(const Interface& iface) {
@@ -256,6 +257,9 @@ static bool is_backbone_interface(const Interface& iface) {
/*static*/ void Transport::jobs() { /*static*/ void Transport::jobs() {
//TRACE("Transport::jobs()"); //TRACE("Transport::jobs()");
// Heap telemetry: snapshot at jobs entry
size_t _jobs_heap_entry = OS::heap_available();
std::vector<Packet> outgoing; std::vector<Packet> outgoing;
std::set<Bytes> path_requests; std::set<Bytes> path_requests;
int count; int count;
@@ -443,6 +447,15 @@ static bool is_backbone_interface(const Interface& iface) {
_packet_hashlist.erase(_packet_hashlist.begin(), iter); _packet_hashlist.erase(_packet_hashlist.begin(), iter);
} }
#ifdef BOUNDARY_MODE
// Cull the boundary mentioned addresses if it has reached its max size
if (_boundary_mentioned_addresses.size() > _boundary_maxsize) {
std::set<Bytes>::iterator iter = _boundary_mentioned_addresses.begin();
std::advance(iter, _boundary_mentioned_addresses.size() - _boundary_maxsize);
_boundary_mentioned_addresses.erase(_boundary_mentioned_addresses.begin(), iter);
}
#endif
// Cull the path request tags list if it has reached its max size // Cull the path request tags list if it has reached its max size
if (_discovery_pr_tags.size() > _max_pr_tags) { if (_discovery_pr_tags.size() > _max_pr_tags) {
std::set<Bytes>::iterator iter = _discovery_pr_tags.begin(); std::set<Bytes>::iterator iter = _discovery_pr_tags.begin();
@@ -656,6 +669,15 @@ static bool is_backbone_interface(const Interface& iface) {
_jobs_running = false; _jobs_running = false;
// Heap telemetry: snapshot at jobs exit
{
size_t _jobs_heap_exit = OS::heap_available();
int _jobs_delta = (int)_jobs_heap_exit - (int)_jobs_heap_entry;
if (_jobs_delta < -64 || _jobs_delta > 64) {
VERBOSEF("[HEAP-TEL] jobs: %d bytes (heap=%u)", _jobs_delta, (uint32_t)_jobs_heap_exit);
}
}
// CBA send announce retransmission packets // CBA send announce retransmission packets
for (auto& packet : outgoing) { for (auto& packet : outgoing) {
packet.send(); packet.send();
@@ -1201,6 +1223,9 @@ static bool is_backbone_interface(const Interface& iface) {
/*static*/ void Transport::inbound(const Bytes& raw, const Interface& interface /*= {Type::NONE}*/) { /*static*/ void Transport::inbound(const Bytes& raw, const Interface& interface /*= {Type::NONE}*/) {
TRACEF("Transport::inbound: received %d bytes", raw.size()); TRACEF("Transport::inbound: received %d bytes", raw.size());
++_packets_received; ++_packets_received;
// Heap telemetry: snapshot at entry
size_t _heap_at_entry = OS::heap_available();
// CBA // CBA
if (_callbacks._receive_packet) { if (_callbacks._receive_packet) {
try { try {
@@ -1440,6 +1465,15 @@ static bool is_backbone_interface(const Interface& iface) {
} }
#endif #endif
// Heap telemetry: snapshot after boundary filter
{
size_t _heap_after_boundary = OS::heap_available();
int _boundary_delta = (int)_heap_after_boundary - (int)_heap_at_entry;
if (_boundary_delta < -64) {
VERBOSEF("[HEAP-TEL] boundary: %d bytes (bma=%u phl=%u)", _boundary_delta, _boundary_mentioned_addresses.size(), _packet_hashlist.size());
}
}
// CBA ACCUMULATES // CBA ACCUMULATES
_packet_hashlist.insert(packet.packet_hash()); _packet_hashlist.insert(packet.packet_hash());
cache_packet(packet); cache_packet(packet);
@@ -2642,6 +2676,21 @@ static bool is_backbone_interface(const Interface& iface) {
} }
} }
// Heap telemetry: snapshot at exit
{
size_t _heap_at_exit = OS::heap_available();
int _inbound_delta = (int)_heap_at_exit - (int)_heap_at_entry;
// Log every 100th packet or when delta exceeds threshold
static uint32_t _tel_pkt_count = 0;
++_tel_pkt_count;
if (_inbound_delta < -64 || (_tel_pkt_count % 100 == 0)) {
VERBOSEF("[HEAP-TEL] inbound: %d bytes (heap=%u pin=%u bma=%u phl=%u lt=%u revr=%u)",
_inbound_delta, (uint32_t)_heap_at_exit, _packets_received,
_boundary_mentioned_addresses.size(), _packet_hashlist.size(),
_link_table.size(), _reverse_table.size());
}
}
_jobs_locked = false; _jobs_locked = false;
} }
@@ -4249,6 +4298,7 @@ TRACE("Transport::write_path_table: buffer size " + std::to_string(Persistence::
interface_announces += interface.announce_queue().size(); interface_announces += interface.announce_queue().size();
} }
VERBOSEF("phl: %u rcp: %u lt: %u pl: %u al: %u tun: %u", _packet_hashlist.size(), _receipts.size(), _link_table.size(), _pending_links.size(), _active_links.size(), _tunnels.size()); VERBOSEF("phl: %u rcp: %u lt: %u pl: %u al: %u tun: %u", _packet_hashlist.size(), _receipts.size(), _link_table.size(), _pending_links.size(), _active_links.size(), _tunnels.size());
VERBOSEF("bla: %u bma: %u", _boundary_local_addresses.size(), _boundary_mentioned_addresses.size());
VERBOSEF("pin: %u pout: %u padd: %u dpr: %u ikd: %u ia: %u\r\n", _packets_received, _packets_sent, _destinations_added, destination_path_responses, Identity::_known_destinations.size(), interface_announces); VERBOSEF("pin: %u pout: %u padd: %u dpr: %u ikd: %u ia: %u\r\n", _packets_received, _packets_sent, _destinations_added, destination_path_responses, Identity::_known_destinations.size(), interface_announces);
_last_memory = memory; _last_memory = memory;

View File

@@ -309,6 +309,37 @@ lib_deps =
${env.lib_deps} ${env.lib_deps}
XPowersLib@^0.2.1 XPowersLib@^0.2.1
[env:heltec_V3_boundary]
platform = espressif32
board = heltec_wifi_lora_32_V3
custom_variant = heltec32v3
board_build.filesystem = littlefs
; Flash / memory layout for 8MB flash + 8MB PSRAM (QSPI)
board_upload.flash_size = 8MB
board_upload.maximum_size = 8388608
board_build.partitions = default_8MB.csv
board_build.flash_mode = qio
board_build.psram_type = qio
board_build.arduino.memory_type = qio_qspi
monitor_speed = 115200
build_flags =
${env.build_flags}
-DBOARD_MODEL=BOARD_HELTEC32_V3
-DBOARD_HAS_PSRAM=1
-DBOUNDARY_MODE
;-DNDEBUG
-DRNS_USE_TLSF=1
-DRNS_USE_ALLOCATOR=1
; --- Boundary mode defaults (override via EEPROM at runtime) ---
; TCP server mode (0=server, 1=client)
-DBOUNDARY_TCP_MODE=0
; TCP listen/connect port
-DBOUNDARY_TCP_PORT=4242
lib_deps =
${env.lib_deps}
XPowersLib@^0.2.1
monitor_filters = esp32_exception_decoder
[env:heltec_wifi_lora_32_V4] [env:heltec_wifi_lora_32_V4]
platform = espressif32 platform = espressif32
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
@@ -342,7 +373,7 @@ build_flags =
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM=1 -DBOARD_HAS_PSRAM=1
-DBOUNDARY_MODE -DBOUNDARY_MODE
-DNDEBUG ;-DNDEBUG
-DRNS_USE_TLSF=1 -DRNS_USE_TLSF=1
-DRNS_USE_ALLOCATOR=1 -DRNS_USE_ALLOCATOR=1
; --- Boundary mode defaults (override via EEPROM at runtime) --- ; --- Boundary mode defaults (override via EEPROM at runtime) ---