Files
HaloKeymind/examples/simple_repeater/main.cpp
T
mikecarper 7a463937ca Reduce shared RAM usage without lowering firmware capacities
Specialize repeater ACL session state, compact recent and flood records, stream Merkle proofs, and release cold OTA buffers safely. Preserve saved formats, stack sizes, table capacities, and OTA source limits. Account for lazy buffers when active and add compatibility, ARM alignment, allocation failure, and queued-response tests.

Validated six representative board/role builds, 1736 native test cases, sanitizer fixtures, and strict documentation. Document the preexisting Wio-E5 Mini flash overflow separately.
2026-10-08 14:41:16 -07:00

634 lines
21 KiB
C++

#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
#include <helpers/ui/StartupScreen.h>
#include <helpers/ui/DisplayPowerSettings.h>
#include <helpers/UsbLoggingWatchdog.h>
#include <helpers/UsbLoggingClientActivity.h>
#include <helpers/HilStartupTrace.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
#endif
#include "MyMesh.h"
#if defined(MESH_SOAK_DIAGNOSTICS)
#include "../../tools/hil/S3SoakDiagnostics.h"
#endif
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#include <helpers/esp32/BootFileSystem.h>
#endif
#if defined(NRF52_PLATFORM)
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
#include <helpers/nrf52/RamFallbackFileSystem.h>
#endif
#if defined(ESP32) && MAX_RECENT_REPEATERS > 0
#include <new>
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(board, display);
static bool display_ready = false;
#endif
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Repeater CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif
#if defined(ESP32) && defined(CONFIG_BT_ENABLED) && !defined(BLE_PIN_CODE)
// A repeater has no Bluetooth transport, but the Arduino SDK is built with the
// BT controller enabled, so its memory stays reserved unless the application
// says otherwise. initArduino() calls esp_bt_controller_mem_release() when
// this weak hook returns false, handing that region to the heap. Note this
// only grows the *runtime heap*: the same reservation is also carved out of
// the linker's dram0_0_seg (0xdb5c on classic ESP32), and no runtime call can
// give those static bytes back. It is what makes the heap-allocated tables
// above comfortable, not a substitute for them.
extern "C" bool btInUse();
extern "C" bool btInUse() { return false; }
#endif
StdRNG fast_rng;
#if MAX_RECENT_REPEATERS > 0
// Compact rows retain the complete prefix/SNR/32-bit age at every capacity.
// All accesses are owned by this loop, including deferred network commands.
#if defined(ESP32)
// Classic ESP32 has a much smaller link-time DRAM window than its runtime heap
// map. Allocate this large, fixed-capacity history at startup so enabling WiFi
// features (including WebConfig) does not overflow that static DRAM window.
// Global constructors run before setup(), while the heap is still
// unfragmented; a failed allocation safely falls back to no history table.
SimpleMeshTables::RecentRepeaterInfo* recent_repeater_storage =
new (std::nothrow) SimpleMeshTables::RecentRepeaterInfo[MAX_RECENT_REPEATERS];
SimpleMeshTables tables(recent_repeater_storage,
recent_repeater_storage ? MAX_RECENT_REPEATERS : 0);
#else
SimpleMeshTables::RecentRepeaterInfo recent_repeater_storage[MAX_RECENT_REPEATERS];
SimpleMeshTables tables(recent_repeater_storage, MAX_RECENT_REPEATERS);
#endif
#else
SimpleMeshTables tables;
#endif
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
#include "../InfrastructureWireless.h"
#if defined(RAK4631_COMBINED_ETHERNET)
// Storage selection and OTA policy belong to the mesh loop. The Ethernet
// worker only handles its own controller and asks these hooks to hand the
// shared WisBlock pins over before/after it runs.
static bool prepareCombinedEthernet(void*, char* error, size_t capacity) {
auto* context = mesh::ota::ota_context_if_active();
if (!context) {
snprintf(error, capacity, "Error: OTA storage is not ready");
return false;
}
return context->prepareRakEthernet(error, capacity);
}
static bool canReleaseCombinedEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
return context && context->canReleaseRakEthernet();
}
static bool releaseCombinedEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
return context && context->releaseRakEthernet();
}
#endif
void halt() {
while (1) ;
}
#if MESH_ENABLE_HOST_CLI
static constexpr size_t LOCAL_SERIAL_COMMAND_MAX =
mesh::HostCliBridge::SERIAL_REPLY_COMMAND_MAX;
#else
static constexpr size_t LOCAL_SERIAL_COMMAND_MAX = 159U;
#endif
static char command[LOCAL_SERIAL_COMMAND_MAX + 2U];
static bool command_overflow = false;
#ifdef ETHERNET_ENABLED
static char ethernet_command[160];
#endif
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long userBtnDownAt = 0;
#define USER_BTN_HOLD_OFF_MILLIS 1500
#endif
void setup() {
mesh::wireless::control().begin(infrastructure_wireless);
mesh::prepareUsbLoggingPort();
Serial.begin(115200);
#if MESH_USB_CONSOLE_COOPERATIVE
mesh::beginUsbLoggingPort();
#endif
#if MESH_PACKET_LOGGING
mesh::serialLogBegin();
#endif
mesh::hilStartupTrace("serial_ready");
mesh::hilStartupTrace("board_begin");
board.begin();
mesh::hilStartupTrace("board_ready");
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
mesh::hilStartupTrace("fs_begin");
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM)
bool volatile_primary_fs = false;
mesh::storage::RamFallbackFileSystem* ram_primary_fs = nullptr;
const auto primary_fs_boot =
mesh::storage::beginInternalPrimaryFilesystemSafely(InternalFS);
if (mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)) {
fs = &InternalFS;
} else {
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
if (ram_primary_fs == nullptr) {
MESH_DEBUG_PRINTLN("InternalFS and RAM fallback initialization failed; rebooting");
board.reboot();
return;
}
fs = &ram_primary_fs->filesystem();
volatile_primary_fs = true;
}
IdentityStore store(*fs, "");
#elif defined(STM32_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#else
#error "need to define filesystem"
#endif
mesh::hilStartupTrace("fs_ready");
#ifdef DISPLAY_CLASS
mesh::hilStartupTrace("display_begin");
mesh::ui::loadDisplayPowerSettings(fs, false);
mesh::ui::StartupScreen startup_screen;
display_ready = display.begin();
if (display_ready) {
startup_screen.begin(&display,
board.isExternalPowered() || board.isUsbHostConnected());
}
mesh::hilStartupTrace("display_ready");
#endif
// Let serial settle after the display is already showing startup.
delay(1000);
#if defined(MESH_DEBUG) && defined(NRF52_PLATFORM)
// give some extra time for serial to settle so
// boot debug messages can be seen on terminal
delay(5000);
#endif
mesh::hilStartupTrace("radio_begin");
int radioinit_attempts = 0;
while (!radio_init()) {
++radioinit_attempts;
MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts);
if (radioinit_attempts >= 3) {
#ifdef RECOVERABLE_EXTERNAL_RADIO
// A remote external-radio node must not churn USB or require a physical
// power cut merely because its radio is temporarily unavailable. Keep
// the MCU alive and retry in place; target radio_init() performs the
// board-specific regulator/reset/wake recovery on each attempt.
mesh::usbConsolePort().println("Radio unavailable; retrying in 60 seconds");
radioinit_attempts = 0;
const uint32_t retry_started = millis();
while (millis() - retry_started < 60000UL) {
#if MESH_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
#endif
#if defined(NRF52_PLATFORM)
board.feedWatchdog();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
delay(10);
}
#else
MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting");
board.reboot();
#endif
}
delay(500);
}
mesh::hilStartupTrace("radio_ready");
fast_rng.begin(radio_driver.getRngSeed());
mesh::hilStartupTrace("identity_begin");
#if defined(NRF52_PLATFORM)
IdentityLoadResult identity_load = volatile_primary_fs
? store.loadResult("_main", the_mesh.self_id)
: mesh::storage::loadIdentityWithPrimaryRecovery(
InternalFS,
[&store]() { return store.loadResult("_main", the_mesh.self_id); });
if (identity_load == IdentityLoadResult::Unreadable) {
ram_primary_fs = mesh::storage::createRamFallbackFileSystem();
if (ram_primary_fs != nullptr) {
fs = &ram_primary_fs->filesystem();
store.useFileSystem(*fs);
volatile_primary_fs = true;
identity_load = IdentityLoadResult::Missing;
}
}
#else
const IdentityLoadResult identity_load =
store.loadResult("_main", the_mesh.self_id);
#endif
const bool needs_identity = identity_load == IdentityLoadResult::Missing
|| (identity_load == IdentityLoadResult::Loaded
&& mesh::hasReservedIdentityPrefix(the_mesh.self_id));
bool identity_ready = identity_load != IdentityLoadResult::Unreadable;
if (needs_identity) {
#ifdef DISPLAY_CLASS
startup_screen.generatingKey();
mesh::ScopedIdentityGenerationProgress progress(
mesh::ui::StartupScreen::progress, &startup_screen);
#endif
MESH_DEBUG_PRINTLN("Generating new keypair");
identity_ready = mesh::generateUsableLocalIdentity(the_mesh.self_id, radio_new_identity);
#ifdef DISPLAY_CLASS
if (identity_ready) startup_screen.savingIdentity();
else startup_screen.starting();
#endif
if (identity_ready) identity_ready = store.saveWithRetry("_main", the_mesh.self_id);
#ifdef DISPLAY_CLASS
startup_screen.starting();
#endif
}
#if defined(ESP32_PLATFORM)
mesh::discardESP32TrueRandom();
#endif
if (!identity_ready) {
MESH_DEBUG_PRINTLN("Identity generation or persistence failed after retries; rebooting");
board.reboot();
return;
}
mesh::hilStartupTrace("identity_ready");
#if defined(ESP32_PLATFORM)
// Identity recovery/persistence has finished before taking the inventory.
// The mesh retains this FS view after setup, so the view has static lifetime.
static mesh::Esp32BootFileSystem boot_fs(*fs);
fs = &boot_fs;
boot_fs.beginInventory();
#endif
// Print the running firmware version at boot so it's visible after an OTA
// reboot without having to issue `ver` manually.
Stream& console = mesh::usbConsolePort();
console.print("Firmware: "); console.print(FIRMWARE_VERSION);
console.print(" (built "); console.print(FIRMWARE_BUILD_DATE); console.println(")");
console.print("Repeater ID: ");
mesh::Utils::printHex(console, the_mesh.self_id.pub_key, PUB_KEY_SIZE); console.println();
command[0] = 0;
#ifdef ETHERNET_ENABLED
ethernet_command[0] = 0;
#endif
#if ENV_INCLUDE_GPS == 1
// With GPS manually off, an authorized location telemetry request may wake
// the receiver only until the first valid fix has been cached.
sensors.setGpsTelemetryStopAfterFix(true);
if (sensors.getLocationProvider() != NULL) {
// Keep GPS awake for at most 10 minutes, then asleep for one week.
sensors.getLocationProvider()->setPowerSavingProfile(
10UL * 60UL, 7UL * 24UL * 60UL * 60UL);
}
#endif
mesh::hilStartupTrace("sensors_begin");
sensors.begin();
mesh::hilStartupTrace("sensors_ready");
mesh::hilStartupTrace("mesh_begin");
the_mesh.begin(fs);
mesh::hilStartupTrace("mesh_ready");
mesh::hilStartupTrace("watchdog_begin");
#if defined(NRF52_PLATFORM)
mesh::loadUsbLoggingWatchdog(fs, !volatile_primary_fs,
[]() -> uint32_t { return rtc_clock.getCurrentTime(); });
#else
mesh::loadUsbLoggingWatchdog(fs, true,
[]() -> uint32_t { return rtc_clock.getCurrentTime(); });
#endif
mesh::hilStartupTrace("watchdog_ready");
#if defined(NRF52_PLATFORM)
if (volatile_primary_fs) {
strncpy(the_mesh.getNodePrefs()->node_name,
mesh::storage::BAD_FILESYSTEM_NODE_NAME,
sizeof(the_mesh.getNodePrefs()->node_name));
the_mesh.getNodePrefs()->node_name[
sizeof(the_mesh.getNodePrefs()->node_name) - 1] = 0;
}
#endif
#ifdef DISPLAY_CLASS
if (display_ready) {
mesh::hilStartupTrace("display_task_begin");
#ifdef WITH_MQTT_BRIDGE
ui_task.setObserverPrefs(the_mesh.getObserverPrefs());
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
ui_task.setActivityWindow(the_mesh.getActivityWindow());
#endif
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
mesh::hilStartupTrace("display_task_ready");
}
#endif
#ifdef ETHERNET_ENABLED
#if defined(RAK4631_COMBINED_ETHERNET)
EthernetCliHooks hooks;
hooks.prepare = prepareCombinedEthernet;
hooks.can_release = canReleaseCombinedEthernet;
hooks.released = releaseCombinedEthernet;
ethernet_configure(hooks); // First prototype defaults OFF on every restart.
#endif
ethernet_start_task();
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
#endif
mesh::hilStartupTrace("boot_complete_begin");
#if defined(ESP32_PLATFORM)
mesh::endEsp32BootFileInventory();
#endif
board.onBootComplete();
mesh::hilStartupTrace("boot_complete_ready");
}
static void __attribute__((noinline)) serviceCommandInterfaces() {
#if defined(MESH_HIL_STARTUP_TRACE) && MESH_HIL_STARTUP_TRACE
static bool first_cli_entry = true;
static bool first_cli_ready = true;
static bool first_cli_blocked = true;
static bool first_cli_rx = true;
if (first_cli_entry) {
first_cli_entry = false;
mesh::hilStartupTrace("cli_first_entry");
}
#endif
bool usb_ready = true;
#if MESH_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
if (mesh::takeUsbTerminalSessionReset()) {
command[0] = 0;
command_overflow = false;
the_mesh.cancelPendingSerialOutput();
}
// A busy USB session cleanup may defer the CLI, never the radio loop.
usb_ready = mesh::tryCompleteUsbTerminalSessionReset();
// Large listings advance from MyMesh::loop without blocking radio service.
usb_ready = usb_ready && !the_mesh.hasPendingSerialOutput()
&& mesh::canAcceptUsbConsoleCommand();
#endif
Stream& console = mesh::usbConsolePort();
#if defined(MESH_HIL_STARTUP_TRACE) && MESH_HIL_STARTUP_TRACE
if (usb_ready && first_cli_ready) {
first_cli_ready = false;
mesh::hilStartupTrace("cli_ready");
} else if (!usb_ready && first_cli_blocked) {
first_cli_blocked = false;
mesh::hilStartupTrace("cli_gate_blocked");
}
if (usb_ready && first_cli_rx && console.available() > 0) {
first_cli_rx = false;
mesh::hilStartupTrace("cli_rx_seen");
}
#endif
// Handle Serial CLI
int len = strlen(command);
bool line_complete = false;
bool overlong_line_complete = false;
size_t read_budget = 256;
while (usb_ready && read_budget-- > 0 && console.available()) {
char c = console.read();
if (c == '\n') continue;
console.print(c);
if (command_overflow) {
if (c == '\r') {
command_overflow = false;
overlong_line_complete = true;
break;
}
continue;
}
if (c == '\r') {
line_complete = true;
break;
}
if ((size_t)len < LOCAL_SERIAL_COMMAND_MAX) {
command[len++] = c;
command[len] = 0;
} else {
// Discard the entire record through its delimiter. Never parse a
// truncated prefix as one command and its tail as a second command.
command[0] = 0;
len = 0;
command_overflow = true;
}
}
if (overlong_line_complete) {
console.print('\n');
console.println(" -> Err - command too long");
command[0] = 0;
return;
}
if (line_complete) {
console.print('\n');
char reply[160];
reply[0] = 0;
if (strlen(command) == static_cast<size_t>(len))
mesh::noteUsbLoggingStatsCommand(command);
#if defined(MESH_SOAK_DIAGNOSTICS)
if (mesh::hil::handleSoakCommand(command, reply, sizeof(reply))) {
console.print(" -> ");
console.println(reply);
command[0] = 0;
return;
}
#endif
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
#if MESH_ENABLE_HOST_CLI
if (!the_mesh.handleHostCliSerialReply(command, reply)) {
the_mesh.handleUsbCommand(command, reply);
}
#else
the_mesh.handleUsbCommand(command, reply);
#endif
}
#else
#if MESH_ENABLE_HOST_CLI
if (!the_mesh.handleHostCliSerialReply(command, reply)) {
the_mesh.handleUsbCommand(command, reply);
}
#else
the_mesh.handleUsbCommand(command, reply);
#endif
#endif
if (reply[0]) {
console.printf(" -> %s\r\n", reply);
}
command[0] = 0; // reset command buffer
}
board.loop(); // let the board feed its watchdog, run periodic housekeeping
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
#if defined(RAK4631_COMBINED_ETHERNET)
// The facade never accesses SPI while startup, renewal, or shutdown owns
// the controller. Keep pending output during a temporary renewal pause.
if (ethernet_take_session_reset() || !ethernet_get_enabled()) {
#else
if (ethernet_take_session_reset() || !ethernet_client.connected()) {
#endif
the_mesh.cancelLocalOutput(ethernet_client);
ethernet_command[0] = 0;
}
if (!the_mesh.hasPendingLocalOutput() && ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
char reply[160];
reply[0] = 0;
if (!ethernet_handle_command(ethernet_command, reply)) {
the_mesh.handleLocalCommand(ethernet_command, reply, ethernet_client);
}
ethernet_send_reply(reply);
ethernet_command[0] = 0;
}
#endif
}
static bool usbLoggingRecoverySafe(void*) {
if (board.isOTAUpdateRunning() || board.isRadioTestActive()
|| radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()
|| !the_mesh.canRecoverUsbLogging()) return false;
const auto usb = mesh::usbLoggingStatus();
// A stale line/listing from a disconnected or stalled host cannot lock out
// recovery forever. Protect a functional live command pump, not diagnostics.
return !usb.reader_connected || usb.stalled
|| (!command[0] && !command_overflow
#if MESH_USB_CONSOLE_COOPERATIVE
&& !the_mesh.hasPendingSerialOutput()
#endif
);
}
void loop() {
mesh::serviceUsbLoggingPort();
mesh::wireless::control().service(millis());
#if defined(NRF52_PLATFORM)
board.feedWatchdog(the_mesh.getNodePrefs()->system_watchdog_enabled != 0);
#endif
serviceCommandInterfaces();
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS)
// Hold the user button to power off the SenseCAP Solar repeater.
int btnState = digitalRead(PIN_USER_BTN);
if (btnState == LOW) {
if (userBtnDownAt == 0) {
userBtnDownAt = millis();
} else if ((unsigned long)(millis() - userBtnDownAt) >= USER_BTN_HOLD_OFF_MILLIS) {
mesh::usbConsolePort().println("Powering off...");
board.powerOff(); // does not return
}
} else {
userBtnDownAt = 0;
}
#endif
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
#if MESH_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#endif
#ifdef TBEAM_1W
board.updateFanControl();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
bool can_power_save = the_mesh.getNodePrefs()->powersaving_enabled
&& !board.isUsbDataConnected()
&& !mesh::isUsbLoggingWatchdogArmed()
&& !mesh::wireless::control().pending();
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
// GPIO wake starts the first poll. Keep debounce and multi-click deadlines
// awake too, so a release cannot strand the remaining 25/280 ms interval.
// The G3 also retains its two-minute button wake interval here.
can_power_save = can_power_save && !user_btn.needsPolling();
#endif
if (can_power_save) {
uint32_t sleep_secs = the_mesh.getPowerSaveSleepSeconds(30);
#ifdef HAS_EXTERNAL_WATCHDOG
if (sleep_secs > 0) external_watchdog.feed();
#endif
#if defined(NRF52_PLATFORM)
if (sleep_secs > 0) {
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
}
#else
if (sleep_secs > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
board.sleep(sleep_secs); // Sleep. Wake up for scheduled jobs or when receiving a LoRa packet
}
#endif
}
#if defined(ESP32_PLATFORM)
if (!can_power_save && the_mesh.getNodePrefs()->powersaving_enabled) {
delay(1); // Idle without suspending USB or the button wake interval.
}
#endif
if (the_mesh.getNodePrefs()->reboot_interval > 0 &&
the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) {
board.reboot();
}
}