Allow Auto, SSD1306 or SH1106 through display.controller on display-enabled RAK4631 and RAK3401 roles. Persist the choice before restart without changing the active screen policy, and let saved Auto override legacy build defaults. Share checked settings I/O and the existing atomic writer to keep Full Companion within its flash boundary while retaining features and existing record formats. Cover save failures, backup recovery, startup selection and ambiguous OLED replies. Validation: 35 targeted host tests passed. RAK4631 Full Companion and RAK3401 unified LoRa OTA repeater build and package checks passed with release-length version strings.
711 lines
24 KiB
C++
711 lines
24 KiB
C++
#include <helpers/RegulatorSettings.h>
|
|
#include <helpers/LedSettings.h>
|
|
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
#include <helpers/IdentityGeneration.h>
|
|
#include <helpers/ui/StartupScreen.h>
|
|
#include <helpers/ui/DisplayPowerSettings.h>
|
|
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
|
#include <helpers/ui/OLEDControllerSettings.h>
|
|
#endif
|
|
#include <helpers/UsbLoggingWatchdog.h>
|
|
#include <helpers/UsbLoggingClientActivity.h>
|
|
#include <helpers/MorseLoopPolicy.h>
|
|
#include <helpers/PowerManagementUtils.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;
|
|
}
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
if (mesh::ota::ota_rak15002_sd_card_present()) {
|
|
snprintf(error, capacity, "Error: RAK15002 SD card fitted; Ethernet shares its IO slot");
|
|
return false;
|
|
}
|
|
if (!context->prepareRakSdForEthernet() ||
|
|
!mesh::ota::rak4631_try_acquire_spi(mesh::ota::RakSharedSpiOwner::Ethernet)) {
|
|
snprintf(error, capacity, "Error: Ethernet bus is locked by OTA activity or storage");
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
return context->prepareRakEthernet(error, capacity);
|
|
#endif
|
|
}
|
|
|
|
static bool canReleaseCombinedEthernet(void*) {
|
|
auto* context = mesh::ota::ota_context_if_active();
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
return context && context->canReleaseRakSdForEthernet();
|
|
#else
|
|
return context && context->canReleaseRakEthernet();
|
|
#endif
|
|
}
|
|
|
|
static bool releaseCombinedEthernet(void*) {
|
|
auto* context = mesh::ota::ota_context_if_active();
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
if (!context || !context->releasedRakSdEthernet()) return false;
|
|
mesh::ota::rak4631_release_spi(mesh::ota::RakSharedSpiOwner::Ethernet);
|
|
return true;
|
|
#else
|
|
return context && context->releaseRakEthernet();
|
|
#endif
|
|
}
|
|
#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
|
|
FILESYSTEM* regulator_prefs_fs = fs;
|
|
#if defined(NRF52_PLATFORM)
|
|
if (volatile_primary_fs) regulator_prefs_fs = nullptr;
|
|
#endif
|
|
mesh::leds::beginSettings(board, regulator_prefs_fs);
|
|
if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) {
|
|
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
|
|
}
|
|
mesh::hilStartupTrace("fs_ready");
|
|
#ifdef DISPLAY_CLASS
|
|
mesh::hilStartupTrace("display_begin");
|
|
mesh::ui::loadDisplayPowerSettings(fs, false);
|
|
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
|
mesh::ui::loadOLEDControllerSettings(regulator_prefs_fs);
|
|
#endif
|
|
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;
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
hooks.storage_conflict = [](void*) { return mesh::ota::ota_rak15002_sd_card_present(); };
|
|
#endif
|
|
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
|
|
}
|
|
|
|
mesh::power::serviceRegulatorSettings();
|
|
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() {
|
|
#if defined(NRF52_PLATFORM)
|
|
board.prepareSleepCycle();
|
|
#endif
|
|
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();
|
|
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
|
if (!morse_timing) 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
|
|
#if defined(NRF52_PLATFORM)
|
|
if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.isDualRadioActive()
|
|
&& !morse_timing) board.prepareProfileIdleCycle();
|
|
#endif
|
|
// Report the guard that really stopped this iteration, without probing
|
|
// later guards or changing their short-circuit hardware access.
|
|
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
|
|
if (!allowed && mesh::powerdiag::enabled())
|
|
mesh::powerdiag::decision(mesh::powerdiag::mask(reason), millis());
|
|
return allowed;
|
|
};
|
|
bool can_power_save = sleep_guard(the_mesh.getNodePrefs()->powersaving_enabled,
|
|
mesh::powerdiag::Reason::PolicyOff)
|
|
&& sleep_guard(!morse_timing, mesh::powerdiag::Reason::Morse)
|
|
&& sleep_guard(!board.isUsbDataConnected(), mesh::powerdiag::Reason::UsbSession)
|
|
&& sleep_guard(!mesh::isUsbLoggingWatchdogArmed(), mesh::powerdiag::Reason::UsbRecovery)
|
|
&& sleep_guard(!mesh::wireless::control().pending(), mesh::powerdiag::Reason::Wireless);
|
|
#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 && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button);
|
|
#endif
|
|
if (can_power_save) {
|
|
uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000);
|
|
uint32_t noise_floor_delay_ms = 0;
|
|
const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms);
|
|
if (noise_floor_wait && noise_floor_delay_ms < sleep_ms) sleep_ms = noise_floor_delay_ms;
|
|
if (noise_floor_wait && noise_floor_delay_ms == 0) {
|
|
if (mesh::powerdiag::enabled())
|
|
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Calibration), millis());
|
|
sleep_ms = 0;
|
|
}
|
|
#ifdef HAS_EXTERNAL_WATCHDOG
|
|
if (sleep_ms > 0) external_watchdog.feed();
|
|
#endif
|
|
#if defined(NRF52_PLATFORM)
|
|
if (sleep_ms > 0 && the_mesh.canIdleDualRadio()) {
|
|
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
|
if (noise_floor_wait) board.sleepMilliseconds(sleep_ms);
|
|
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
|
|
}
|
|
#else
|
|
if (sleep_ms > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
|
|
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
|
|
board.sleepMilliseconds(sleep_ms); // Wake for scheduled jobs or a LoRa packet.
|
|
} else if (sleep_ms > 0 && mesh::powerdiag::enabled()) {
|
|
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if defined(ESP32_PLATFORM)
|
|
if (!morse_timing && !can_power_save && !the_mesh.isDualRadioActive()
|
|
&& the_mesh.getNodePrefs()->powersaving_enabled) {
|
|
// Live radio2 needs the scanner's zero-delay polling cadence.
|
|
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();
|
|
}
|
|
}
|