Files
HaloKeymind/examples/simple_room_server/main.cpp
T
mikecarper 8a8c80ee6a Unblock LR2021 retunes and add saved crystal calibration
Repeated preamble/header activity can keep a requested primary or TempRadio
change busy indefinitely. Bound unfinished-RX admission for accepted LR2021
changes while preserving completed packets, TX, BUSY and exclusive owners.
Ordinary scans and CAD keep their existing admission rules. Expose the failing
stage and native status through get radio.apply.

Add saved radio.xtrim capacitor calibration and measured radio.freqerror in
signed Hz from the last valid LoRa packet. Preserve native trim by default,
restore explicit calibration after resets/Morse, verify saves before applying,
wait two seconds for the acknowledgement, and roll back failed changes.

Calibration motivation and prior upstream work:
https://github.com/meshcore-dev/MeshCore/issues/3365
https://github.com/recrof/MeshCore/tree/lr2021-trim
Packet-status format follows Semtech's LR20xx driver and RadioLib staging.

Tests execute production activity/admission and transaction paths with noisy
RX, completion races, protected owners, failed setters, RX restart and save
rollback. Physical W12 retuning and RF calibration still require confirmation.
2026-10-26 15:55:36 -07:00

531 lines
18 KiB
C++

#include <helpers/RegulatorSettings.h>
#if defined(USE_LR2021)
#include <helpers/radiolib/LR2021CalibrationSettings.h>
#endif
#include <helpers/sensors/SolarXiaoPower.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(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
#include <helpers/ui/HeltecV4DisplaySettings.h>
#endif
#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>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
#endif
#include "MyMesh.h"
#if defined(OTA_BOOTLOADER_MIGRATION)
#include <helpers/ota/BootloaderMigration.h>
#endif
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#include <helpers/esp32/ProfileIdleSleep.h>
#include <helpers/esp32/BootFileSystem.h>
#endif
#if defined(NRF52_PLATFORM)
#include <helpers/nrf52/InternalPrimaryFsBoot.h>
#include <helpers/nrf52/RamFallbackFileSystem.h>
#endif
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Room Server CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
#ifdef DISPLAY_TOUCH_TOGGLE
static UITask ui_task(board, display);
#else
static UITask ui_task(display);
#endif
static bool display_ready = false;
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
#include "../InfrastructureWireless.h"
#if defined(ETHERNET_ENABLED) && defined(OTA_RAK15002_SD_STORE)
#include <helpers/ota/OtaContext.h>
// The RAK15002 and RAK13800 occupy the same IO-slot chip select. Ethernet is
// opt-in, and only the mesh thread may release cached sources/staging owners
// and reserve the bus before its asynchronous DHCP worker starts.
static bool prepareSdEthernet(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 (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;
}
static bool canReleaseSdEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
return context && context->canReleaseRakSdForEthernet();
}
static bool releaseSdEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
if (!context || !context->releasedRakSdEthernet()) return false;
mesh::ota::rak4631_release_spi(mesh::ota::RakSharedSpiOwner::Ethernet);
return true;
}
#endif
void halt() {
while (1) ;
}
static char command[MAX_POST_TEXT_LEN+1];
#ifdef ETHERNET_ENABLED
static char ethernet_command[MAX_POST_TEXT_LEN+1];
#endif
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
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
board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.begin();
#endif
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(RP2040_PLATFORM)
LittleFS.begin();
fs = &LittleFS;
IdentityStore store(LittleFS, "/identity");
store.begin();
#elif defined(ESP32)
SPIFFS.begin(true);
fs = &SPIFFS;
IdentityStore store(SPIFFS, "/identity");
#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 defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
mesh::solar::beginSettings(board, regulator_prefs_fs, MESH_ENABLE_TELEMETRY_HISTORY);
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
}
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
#if defined(USE_LR2021)
mesh::lr2021::loadLR2021CalibrationSettings(regulator_prefs_fs, radio_driver);
#endif
#ifdef DISPLAY_CLASS
mesh::ui::loadDisplayPowerSettings(fs, false);
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
mesh::ui::loadHeltecV4DisplaySettings(regulator_prefs_fs, display);
#endif
#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());
}
#endif
// Let serial settle after the display is already showing startup.
delay(1000);
int radioinit_attempts = 0;
while (!radio_init()) {
++radioinit_attempts;
MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts);
if (radioinit_attempts >= 3) {
MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting");
board.reboot();
}
delay(500);
}
fast_rng.begin(radio_driver.getRngSeed());
#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
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;
}
#if defined(ESP32_PLATFORM)
// Identity writes have finished; this FS view remains alive after setup.
static mesh::Esp32BootFileSystem boot_fs(*fs);
fs = &boot_fs;
boot_fs.beginInventory();
#endif
Stream& console = mesh::usbConsolePort();
console.print("Room 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
sensors.begin();
the_mesh.begin(fs);
#if defined(OTA_BOOTLOADER_MIGRATION)
mesh::ota::bootloader_migration_begin(volatile_primary_fs ? nullptr : fs);
#endif
#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
#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 defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
// Initialize the UI dependency even when auto detection found no screen.
// A later saved display.type change can enable it without restarting.
display_ready = true;
#endif
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
#if defined(OTA_RAK15002_SD_STORE)
EthernetCliHooks hooks;
hooks.prepare = prepareSdEthernet;
hooks.can_release = canReleaseSdEthernet;
hooks.released = releaseSdEthernet;
hooks.storage_conflict = [](void*) { return mesh::ota::ota_rak15002_sd_card_present(); };
ethernet_configure(hooks); // Shared IO slot 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
#if defined(ESP32_PLATFORM)
mesh::endEsp32BootFileInventory();
#endif
board.onBootComplete();
}
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();
return !usb.reader_connected || usb.stalled
|| (!command[0]
#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
bool usb_ready = true;
#if MESH_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbLoggingPort();
mesh::serviceUsbTerminalPort();
if (mesh::takeUsbTerminalSessionReset()) {
command[0] = 0;
the_mesh.cancelPendingSerialOutput();
}
usb_ready = mesh::tryCompleteUsbTerminalSessionReset();
usb_ready = usb_ready && !the_mesh.hasPendingSerialOutput()
&& mesh::canAcceptUsbConsoleCommand();
#endif
Stream& console = mesh::usbConsolePort();
int len = strlen(command);
// `command` must stay NUL-terminated within its bounds. If it ever isn't,
// strlen() above can return >= sizeof(command) and the loop below would then
// index past the buffer, so clamp defensively.
if (len >= (int)sizeof(command)) {
command[0] = 0;
len = 0;
}
while (usb_ready && console.available() && len < sizeof(command)-1) {
char c = console.read();
if (c != '\n') {
command[len++] = c;
command[len] = 0;
}
console.print(c);
}
if (len == sizeof(command)-1) { // buffer full: treat as a completed line
command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer
command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated
}
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
reply[0] = 0;
if (len < static_cast<int>(sizeof(command) - 1)
&& strlen(command) == static_cast<size_t>(len - 1))
mesh::noteUsbLoggingStatsCommand(command);
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleUsbCommand(command, reply);
}
#else
the_mesh.handleUsbCommand(command, reply);
#endif
if (reply[0]) {
console.printf(" -> %s\r\n", reply);
}
command[0] = 0; // reset command buffer
}
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
#if defined(OTA_RAK15002_SD_STORE)
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
the_mesh.loop();
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
#if defined(USE_LR2021)
if (!morse_timing) mesh::lr2021::loopLR2021CalibrationSettings(radio_driver);
#endif
if (!morse_timing) sensors.loop();
#ifdef DISPLAY_CLASS
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
if (display.selectedType() != mesh::ui::HeltecV4DisplayType::None) ui_task.loop();
#else
if (display_ready) ui_task.loop();
#endif
#endif
rtc_clock.tick();
mesh::power::serviceRegulatorSettings();
#if defined(MESH_SOLARXIAO_POWER) && MESH_SOLARXIAO_POWER
mesh::solar::service();
#endif
board.loop();
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
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis(), false, the_mesh.isDualRadioActive());
#endif
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)
can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button);
#endif
if (can_power_save) {
#if defined(ESP32_PLATFORM)
const uint32_t sleep_budget_started_us = static_cast<uint32_t>(esp_timer_get_time());
#endif
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());
#if defined(ESP32_PLATFORM)
if (the_mesh.isDualRadioActive()) {
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, sleep_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::DualRadio), millis());
} else
#endif
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); // Keep USB and the button wake interval serviced while idle.
}
#endif
}