mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-25 16:13:36 +00:00
Skip redundant modulation writes during owned fast RX retunes when the last acknowledged SF/BW/CR/LDRO tuple matches. Invalidate that cache after ordinary setters, failed writes, and lifecycle changes. Include the remaining scan, preamble, settling, and memory-soak experiments, their collectors, validation notes, and original capture records. Preserve capture bytes across checkouts and keep private soak credentials local. Run lab collector and compiled contract tests in CI. Update the expectation, profile mapping, and result-buffer tests for the extended lab tools, and make the private WiFi override header optional for ordinary soak diagnostics. Validation: 145 host tests passed from the staged source snapshot. Clean heltec_v4_repeater and Xiao_S3_WIO_companion_radio_usb builds passed their RAM/flash gates. All 229 staged capture files retain their original bytes; all 75 local documentation links resolve in the clean snapshot.
428 lines
13 KiB
C++
428 lines
13 KiB
C++
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
#include <helpers/IdentityGeneration.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>
|
|
#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
|
|
#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);
|
|
|
|
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::prepareUsbLoggingPort();
|
|
Serial.begin(115200);
|
|
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
|
|
mesh::beginUsbLoggingPort();
|
|
#endif
|
|
#if MESH_PACKET_LOGGING
|
|
mesh::serialLogBegin();
|
|
#endif
|
|
delay(1000);
|
|
|
|
board.begin();
|
|
|
|
#ifdef HAS_EXTERNAL_WATCHDOG
|
|
external_watchdog.begin();
|
|
#endif
|
|
|
|
#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
|
|
|
|
#ifdef DISPLAY_CLASS
|
|
display_ready = display.begin();
|
|
if (display_ready) {
|
|
display.turnOff(); // Stay dark until the saved display policy is loaded.
|
|
}
|
|
#endif
|
|
|
|
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_ESP32_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);
|
|
}
|
|
|
|
fast_rng.begin(radio_driver.getRngSeed());
|
|
|
|
FILESYSTEM* fs;
|
|
#if defined(NRF52_PLATFORM) || 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
|
|
const bool needs_identity = !store.load("_main", the_mesh.self_id)
|
|
|| mesh::hasReservedIdentityPrefix(the_mesh.self_id);
|
|
bool identity_ready = true;
|
|
if (needs_identity) {
|
|
MESH_DEBUG_PRINTLN("Generating new keypair");
|
|
identity_ready = mesh::generateUsableLocalIdentity(the_mesh.self_id, radio_new_identity);
|
|
if (identity_ready) store.save("_main", the_mesh.self_id);
|
|
}
|
|
|
|
#if defined(ESP32_PLATFORM)
|
|
mesh::discardESP32TrueRandom();
|
|
#endif
|
|
if (!identity_ready) {
|
|
MESH_DEBUG_PRINTLN("Identity generation exhausted all attempts; rebooting");
|
|
board.reboot();
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
if (sensors.getLocationProvider() != NULL) {
|
|
// Keep GPS awake for at most 10 minutes, then asleep for one day.
|
|
sensors.getLocationProvider()->setPowerSavingProfile(600, 86400);
|
|
}
|
|
#endif
|
|
|
|
sensors.begin();
|
|
|
|
the_mesh.begin(fs);
|
|
|
|
#ifdef DISPLAY_CLASS
|
|
if (display_ready) {
|
|
#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);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ETHERNET_ENABLED
|
|
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
|
|
|
|
board.onBootComplete();
|
|
}
|
|
|
|
static void __attribute__((noinline)) serviceCommandInterfaces() {
|
|
bool usb_ready = true;
|
|
#if MESH_ESP32_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();
|
|
// 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 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
|
|
}
|
|
|
|
#ifdef ETHERNET_ENABLED
|
|
ethernet_loop_maintain();
|
|
if (ethernet_take_session_reset() || !ethernet_client.connected()) {
|
|
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
|
|
}
|
|
|
|
void loop() {
|
|
#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_ESP32_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();
|
|
#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();
|
|
}
|
|
}
|