mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 19:41:59 +00:00
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.
634 lines
21 KiB
C++
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();
|
|
}
|
|
}
|