mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-13 04:55:33 +00:00
Integrate ExpressLRS TX module support and its stacked ESP32 heap changes. Use the approved Linkflow calibration (17-30 dBm), preserve the PA drive and output path after radio recovery, and keep LoRa OTA enabled. Preserve stored ACLs and filters on allocation failure, release owned client/filter buffers, service heap OTA contexts on Companions, release self-serving workspaces after TempRadio, and reset staged-resume state when a context is released. Keep manual staging and active operations alive. Account for all moved allocations in the runtime RAM gate. Validation: 1,393 native cases; radio-power, heap-context, ACL persistence, shared-queue transfer, display/inbox, radio receive, and memory regressions. Firmware builds passed for Linkflow, Heltec V2 Companion, T-Beam MQTT repeater, Heltec V4 R8 MQTT repeater, RAK4631 repeater, and Indicator Full. Physical verification awaits access to the currently offline lab Pi.
417 lines
12 KiB
C++
417 lines
12 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(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;
|
|
#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();
|
|
}
|
|
}
|