From 7982ac7fb32b44071fec21ec0f60bb0c815b7c28 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:44:48 +1000 Subject: [PATCH 001/127] nRF52840 Power Management Stage 1 (Boot Lock) - v2.3 preparation work This commit lays some of the ground work for the upcoming power management fix that allows users to configure the boot lock behaviour. It is *not* the full fix yet. The goal is to break up the larger commit into smaller chunks so it is easier to review and softer to merge. Improvements: - MainBoard: Add bool to check if power management has been initialised (later to be consumed by CommonCLI to validate we can safely configure the settings) - MainBoard: Add bool to validate LPCOMP is supported on the board (gates LPCOMP config so it doesnt wedge on an unsupported board) - NRF52Board: Rename initPowerMgr() -> pwrmgtInit() to align with the "pwrmgt" prefix being used by any power management related functions in the upcoming new version - NRF52Board: Add a shutdown reason for "None" so the `get pwrmgt.bootreason` command doesnt erroneously return "Unknown" - NRF52Board: Add gate in configureVoltageWake to not arm LPCOMP when power management isn't initialised or the board doesn't support LPCOMP (i.e. we havent defined the AIN pin) - NRF52Board: Reference the LPCOMP AIN pin directly instead of from the per-board definitions to align to the upcoming deprecation of the per-board static configs - NRF52Board: Drop LPCOMP hysteresis as it broadens the wake voltage too much and makes the board less likely to self-recover - NRF52Board: Separate LPCOMP and VBUS wake arm into their own functions --- src/MeshCore.h | 2 ++ src/helpers/NRF52Board.cpp | 47 ++++++++++++++++++++++---------------- src/helpers/NRF52Board.h | 11 ++++++--- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/MeshCore.h b/src/MeshCore.h index 89e60b1f7..705878333 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -69,8 +69,10 @@ public: virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) + virtual bool isPwrMgtInitialised() const { return false; } virtual bool isExternalPowered() { return false; } virtual uint16_t getBootVoltage() { return 0; } + virtual bool getWakeLpcompSupported() const { return false; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5fb1e55ed..84ab04487 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -38,7 +38,8 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() { g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; } -void NRF52Board::initPowerMgr() { +void NRF52Board::pwrmgtInit() { + if (pwrmgt_initialised) return; // Copy early-captured register values reset_reason = g_nrf52_reset_reason; shutdown_reason = g_nrf52_shutdown_reason; @@ -65,6 +66,7 @@ void NRF52Board::initPowerMgr() { MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)", getResetReasonString(reset_reason), (unsigned long)reset_reason); } + pwrmgt_initialised = true; } const char* NRF52Board::getResetReasonString(uint32_t reason) { @@ -89,6 +91,7 @@ const char* NRF52Board::getResetReasonString(uint32_t reason) { const char* NRF52Board::getShutdownReasonString(uint8_t reason) { switch (reason) { + case SHUTDOWN_REASON_NONE: return "None"; case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage"; case SHUTDOWN_REASON_USER: return "User Request"; case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection"; @@ -97,7 +100,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { } bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { - initPowerMgr(); + pwrmgtInit(); // Read boot voltage boot_voltage_mv = getBattMilliVolts(); @@ -164,24 +167,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) { NVIC_SystemReset(); } -void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { +void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t lpcomp_refsel) { + pwrmgtWakeArmVbus(); + if (!isPwrMgtInitialised()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: Power Management not initialised"); + return; + } + if (!getWakeLpcompSupported()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: LPCOMP unsupported on variant"); + return; + } // LPCOMP is not managed by SoftDevice - direct register access required // Halt and disable before reconfiguration NRF_LPCOMP->TASKS_STOP = 1; NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled; // Select analog input (AIN0-7 maps to PSEL 0-7) - NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; + NRF_LPCOMP->PSEL = ((uint32_t)PWRMGT_LPCOMP_AIN << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; + NRF_LPCOMP->REFSEL = ((uint32_t)lpcomp_refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; // Detect UP events (voltage rises above threshold for battery recovery) NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up; - // Enable 50mV hysteresis for noise immunity - NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV; - // Clear stale events/interrupts before enabling wake NRF_LPCOMP->EVENTS_READY = 0; NRF_LPCOMP->EVENTS_DOWN = 0; @@ -195,23 +204,22 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled; NRF_LPCOMP->TASKS_START = 1; - // Wait for comparator to settle before entering SYSTEMOFF + // Wait for comparator to settle for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) { delayMicroseconds(50); } - if (refsel == 7) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel); - } else if (refsel <= 6) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)", - ain_channel, refsel + 1); + if (lpcomp_refsel == 7) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN); + } else if (lpcomp_refsel <= 6) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1); } else { - uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1); - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)", - ain_channel, ref_num); + uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num); } +} - // Configure VBUS (USB power) wake alongside LPCOMP +void NRF52Board::pwrmgtWakeArmVbus() { uint8_t sd_enabled = 0; sd_softdevice_is_enabled(&sd_enabled); if (sd_enabled) { @@ -220,8 +228,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_POWER->EVENTS_USBDETECTED = 0; NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk; } - - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed"); } #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f974..0d31726bc 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -25,15 +25,15 @@ struct PowerMgtConfig { #endif class NRF52Board : public mesh::MainBoard { -#ifdef NRF52_POWER_MANAGEMENT - void initPowerMgr(); -#endif +private: + bool pwrmgt_initialised = false; protected: uint8_t startup_reason; char *ota_name; #ifdef NRF52_POWER_MANAGEMENT + void pwrmgtInit(); uint32_t reset_reason; // RESETREAS register value uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) @@ -41,6 +41,7 @@ protected: bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void pwrmgtWakeArmVbus(); virtual void initiateShutdown(uint8_t reason); #endif @@ -63,6 +64,10 @@ public: uint8_t getShutdownReason() const override { return shutdown_reason; } const char* getResetReasonString(uint32_t reason) override; const char* getShutdownReasonString(uint8_t reason) override; + bool isPwrMgtInitialised() const override { return pwrmgt_initialised; } + #ifdef PWRMGT_LPCOMP_AIN + bool getWakeLpcompSupported() const override { return true; } + #endif #endif }; From 3e828277066f97fbed0e15b67629c95353716116 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 7 Aug 2026 21:47:02 -0400 Subject: [PATCH 002/127] thinknode_m8: initial support --- boards/thinknode_m8.json | 53 +++++++ examples/companion_radio/ui-new/UITask.cpp | 48 ++++++- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 52 ++++++- src/helpers/ui/GxEPDDisplay.h | 10 ++ src/helpers/ui/MomentaryButton.h | 4 +- src/helpers/ui/RotaryInputGPIO.cpp | 34 +++++ src/helpers/ui/RotaryInputGPIO.h | 21 +++ variants/thinknode_m8/ThinkNodeM8Board.cpp | 38 +++++ variants/thinknode_m8/ThinkNodeM8Board.h | 50 +++++++ variants/thinknode_m8/platformio.ini | 160 +++++++++++++++++++++ variants/thinknode_m8/target.cpp | 36 +++++ variants/thinknode_m8/target.h | 30 ++++ variants/thinknode_m8/variant.cpp | 39 +++++ variants/thinknode_m8/variant.h | 141 ++++++++++++++++++ 15 files changed, 705 insertions(+), 12 deletions(-) create mode 100644 boards/thinknode_m8.json create mode 100644 src/helpers/ui/RotaryInputGPIO.cpp create mode 100644 src/helpers/ui/RotaryInputGPIO.h create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.cpp create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.h create mode 100644 variants/thinknode_m8/platformio.ini create mode 100644 variants/thinknode_m8/target.cpp create mode 100644 variants/thinknode_m8/target.h create mode 100644 variants/thinknode_m8/variant.cpp create mode 100644 variants/thinknode_m8/variant.h diff --git a/boards/thinknode_m8.json b/boards/thinknode_m8.json new file mode 100644 index 000000000..acd7f89c0 --- /dev/null +++ b/boards/thinknode_m8.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m8", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M8", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "elecrow thinknode m8", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "", + "vendor": "ELECROW" +} diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31e..d5e95d157 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,10 +2,15 @@ #include #include "../MyMesh.h" #include "target.h" +#include #ifdef WIFI_SSID #include #endif +#ifndef UI_TZ_OFFSET + #define UI_TZ_OFFSET 0 +#endif + #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -23,7 +28,7 @@ #define UI_RECENT_LIST_SIZE 4 #endif -#if UI_HAS_JOYSTICK +#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT #define PRESS_LABEL "press Enter" #else #define PRESS_LABEL "long press" @@ -224,7 +229,19 @@ public: display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.drawTextCentered(display.width() / 2, 22, tmp); - + + #ifdef UI_SHOW_CLOCK + display.setTextSize(3); + uint32_t now = _rtc->getCurrentTime(); + int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... + now += (int32_t)tz * 3600; + DateTime dt (now); + sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); + display.drawTextCentered(display.width() / 2, 60, tmp); + display.setTextSize(1); + sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); + display.drawTextCentered(display.width() / 2, 80, tmp); + #endif #ifdef WIFI_SSID IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); @@ -234,13 +251,21 @@ public: if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); + #ifdef UI_SHOW_CLOCK + display.drawTextCentered(display.width() / 2, 110, "< Connected >"); + #else display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - + #endif } else if (the_mesh.getBLEPin() != 0) { // BT pin display.setColor(UIColor::warning_txt); - display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); + #ifdef UI_SHOW_CLOCK + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 110, tmp); + #else + display.setTextSize(2); display.drawTextCentered(display.width() / 2, 43, tmp); + #endif } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -720,6 +745,9 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + display.forceFullRefresh(); + display.clear(); + display.endFrame(); // Power off board including radio, display, GPS and components _board->powerOff(); } @@ -760,6 +788,17 @@ void UITask::loop() { } #elif defined(PIN_USER_BTN) int ev = user_btn.check(); + #ifdef UI_HAS_NAV_INPUT + if (ev == BUTTON_EVENT_CLICK) { + c = checkDisplayOn(KEY_ENTER); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + display.turnOff(); + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + c = handleDoubleClick(KEY_SELECT); + } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { + c = handleTripleClick(KEY_SELECT); + } + #else if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { @@ -769,6 +808,7 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } + #endif #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index b76a1b6ca..3e9e2dde8 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -23,6 +23,7 @@ public: virtual bool isOn() = 0; virtual bool isEink() { return false; } // default to non-eink, override in eink drivers + virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 13dafa345..11f78b570 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,10 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +#ifndef EPD_WASHING_MACHINE_CYCLES + #define EPD_WASHING_MACHINE_CYCLES 0 +#endif + // Color scheme ColorVal UIColor::window_bkg = GxEPD_WHITE; ColorVal UIColor::title_bkg = GxEPD_WHITE; @@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE; ColorVal UIColor::popup_txt = GxEPD_BLACK; ColorVal UIColor::corp_blue = GxEPD_BLACK; - bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() { display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); setTextSize(1); // Default to size 1 - display.setPartialWindow(0, 0, display.width(), display.height()); - display.fillScreen(GxEPD_WHITE); - display.display(true); + display.setFullWindow(); + + for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) { + display.fillScreen(GxEPD_BLACK); + display.display(false); + delay(2000); + display.fillScreen(GxEPD_WHITE); + display.display(false); + delay(2000); + } + + display.setPartialWindow(0, 0, display.width(), display.height()); + resetPartialRefreshCounter(); + #if DISP_BACKLIGHT digitalWrite(DISP_BACKLIGHT, LOW); pinMode(DISP_BACKLIGHT, OUTPUT); #endif _init = true; + _isOn = true; return true; } @@ -55,7 +70,11 @@ void GxEPDDisplay::turnOn() { #elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif - _isOn = true; + if (!_isOn) { + forceFullRefresh(); + _isOn = true; + last_display_crc_value=0; + } } void GxEPDDisplay::turnOff() { @@ -65,6 +84,11 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; + display.setFullWindow(); + display.fillScreen(GxEPD_WHITE); + display.display(true); + forceFullRefresh(); + display.powerOff(); } void GxEPDDisplay::clear() { @@ -77,6 +101,12 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); + if (_cycles_before_full_refresh <= 0) { + display.setPartialWindow(0, 0, display.width(), display.height()); + } else { + display.setFullWindow(); + display.writeScreenBuffer(); + } } void GxEPDDisplay::setTextSize(int sz) { @@ -178,9 +208,19 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) { } void GxEPDDisplay::endFrame() { + if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - display.display(true); + if (_cycles_before_full_refresh == 0) { + display.display(false); + display.writeScreenBuffer(); + resetPartialRefreshCounter(); + } else { + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; + } + } last_display_crc_value = crc; } } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c653eac4b..8a37b72cc 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,6 +16,12 @@ #include "DisplayDriver.h" +#ifndef EINK_MAX_PARTIAL_REFRESH + // 60 to prevent ghosting when refreshing every minute ... + // set to -1 to disable the counter + #define EINK_MAX_PARTIAL_REFRESH -1 +#endif + class GxEPDDisplay : public DisplayDriver { #if defined(EINK_DISPLAY_MODEL) @@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver { uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; + int _cycles_before_full_refresh; + int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after public: #if defined(EINK_DISPLAY_MODEL) @@ -48,6 +56,8 @@ public: bool isOn() override { return _isOn; } bool isEink() override { return true; } + void forceFullRefresh() override { _cycles_before_full_refresh = 0; }; + void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; void turnOn() override; void turnOff() override; void clear() override; diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h index 358a343b0..351545cef 100644 --- a/src/helpers/ui/MomentaryButton.h +++ b/src/helpers/ui/MomentaryButton.h @@ -25,8 +25,8 @@ class MomentaryButton { public: MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true); MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold); - void begin(); - int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* + virtual void begin(); + virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state) uint8_t getPin() { return _pin; } bool isPressed() const; diff --git a/src/helpers/ui/RotaryInputGPIO.cpp b/src/helpers/ui/RotaryInputGPIO.cpp new file mode 100644 index 000000000..b094fe498 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.cpp @@ -0,0 +1,34 @@ +#include "RotaryInputGPIO.h" + +bool RotaryInputGPIO::begin() { + + if (_pin_a >= 0) { + pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT); + } + if (_pin_b >= 0) { + pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT); + } + + b_prec = digitalRead(_pin_b); + + return true; +} + +RotaryInputEvent RotaryInputGPIO::poll() { + RotaryInputEvent ev = RotaryInputEvent::None; + bool a = digitalRead(_pin_a); + bool b = digitalRead(_pin_b); + + // this is the simplest scheme and it works well + // for thinknode M8, just read A when B rises ;) + if (!b_prec && b) { // rising edge of A + if (a) { + ev = RotaryInputEvent::Next; + } else { + ev = RotaryInputEvent::Prev; + } + } + b_prec = b; + + return ev; +} \ No newline at end of file diff --git a/src/helpers/ui/RotaryInputGPIO.h b/src/helpers/ui/RotaryInputGPIO.h new file mode 100644 index 000000000..16b7e46ab --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "RotaryInput.h" + +#define BUTTON_EVENT_UP 5 +#define BUTTON_EVENT_DOWN 6 + +class RotaryInputGPIO: public RotaryInput { + int8_t _pin_a; + bool _pull_a; + int8_t _pin_b; + bool _pull_b; + bool b_prec; +public: + RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {} + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return true;} +}; diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp new file mode 100644 index 000000000..2ca393d98 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -0,0 +1,38 @@ +#include +#include + +#include "ThinkNodeM8Board.h" + +#ifdef THINKNODE_M8 + +void ThinkNodeM8Board::begin() { + NRF52Board::begin(); + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM8Board::getBattMilliVolts() { + int adcvalue = 0; + + digitalWrite(ADC_EN, HIGH); + analogReference(AR_INTERNAL_2_4); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + digitalWrite(ADC_EN, LOW); + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} +#endif diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h new file mode 100644 index 000000000..73aeb1f5d --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +// built-ins +#define VBAT_MV_PER_LSB (0.5859375F) // 2.4V ADC range and 12-bit ADC resolution = 2400mV/4096 + +#define VBAT_DIVIDER (0.57F) // 150K + 150K voltage divider on VBAT +#define VBAT_DIVIDER_COMP (1.75F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (4) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class ThinkNodeM8Board : public NRF52Board { +public: + ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {} + void begin(); + uint16_t getBattMilliVolts() override; + + #if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } + #endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode-M8"; + } + + void shutdownPeripherals() override { + // power off board + NRF52Board::shutdownPeripherals(); + + // make sure every gate is closed + digitalWrite(DISP_EN, LOW); + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(SX126X_ANT_SW, LOW); + digitalWrite(ADC_EN, LOW); + + #ifdef PIN_BUTTON1 // Use BTN to go out of sleep + nrf_gpio_cfg_sense_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + #endif + } +}; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini new file mode 100644 index 000000000..9f1595c55 --- /dev/null +++ b/variants/thinknode_m8/platformio.ini @@ -0,0 +1,160 @@ +[ThinkNode_M8] +extends = nrf52_base +board = thinknode_m8 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m8 + -D THINKNODE_M8=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=25 + -D P_LORA_DIO_2=32 + -D P_LORA_NSS=21 + -D P_LORA_RESET=24 + -D P_LORA_BUSY=32 # DIO2 + -D P_LORA_SCLK=19 + -D P_LORA_MISO=22 + -D P_LORA_MOSI=20 + -D UI_SHOW_CLOCK=1 + -D UI_HAS_ROTARY_INPUT=1 + -D UI_HAS_NAV_INPUT=1 + -D UI_RECENT_LIST_SIZE=9 + -D UI_TZ_OFFSET=-4 # GMT-4 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 + -D DISABLE_DIAGNOSTIC_OUTPUT + -D SX126X_POWER_EN=37 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/thinknode_m8> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 +debug_tool = jlink +upload_protocol = nrfutil + +[env:ThinkNode_M8_repeater] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_room_server] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_room_server/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_companion_radio_ble] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 +# -D BLE_DEBUG_LOGGING=1 + -D DISPLAY_ROTATION=4 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=33 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D QSPIFLASH=1 + -D ENV_INCLUDE_GPS=1 +; -D GPS_NMEA_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.9 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_companion_radio_usb] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_ROTATION=4 + -D QSPIFLASH=1 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=6 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.2 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_kiss_modem] +extends = ThinkNode_M8 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp new file mode 100644 index 000000000..ac4ac100e --- /dev/null +++ b/variants/thinknode_m8/target.cpp @@ -0,0 +1,36 @@ +#include +#include "target.h" +#include +#include +#include + +ThinkNodeM8Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h new file mode 100644 index 000000000..c3402a01a --- /dev/null +++ b/variants/thinknode_m8/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include + #include +#endif + +extern ThinkNodeM8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern RotaryInputGPIO rotary_input; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m8/variant.cpp b/variants/thinknode_m8/variant.cpp new file mode 100644 index 000000000..e15c08420 --- /dev/null +++ b/variants/thinknode_m8/variant.cpp @@ -0,0 +1,39 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const int MISO = PIN_SPI1_MISO; +const int MOSI = PIN_SPI1_MOSI; +const int SCK = PIN_SPI1_SCK; + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + pinMode(DISP_EN, OUTPUT); + digitalWrite(DISP_EN, HIGH); + + pinMode(PIN_BUTTON1, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_A, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_B, INPUT_PULLDOWN); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + + // shutdown gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // disable at startup + + pinMode(SX126X_ANT_SW, OUTPUT); // ANT_SW + digitalWrite(SX126X_ANT_SW, HIGH); + + pinMode(ADC_EN, OUTPUT); + digitalWrite(ADC_EN, LOW); + +} diff --git a/variants/thinknode_m8/variant.h b/variants/thinknode_m8/variant.h new file mode 100644 index 000000000..9c36c772b --- /dev/null +++ b/variants/thinknode_m8/variant.h @@ -0,0 +1,141 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_TXCO (21) +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_PWR_EN (13) // I2C + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (1.75F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (2.4) + +#define ADC_EN (40) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (22) +#define PIN_SPI_MOSI (20) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (21) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs +#define LED_BLUE (-1) + +#define LED_BUILTIN LED_BLUE +#define LED_PIN LED_BUILTIN +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) +#define PIN_BUTTON1_A (8) // Rotary button/Encoder +#define PIN_BUTTON1_B (41) +#define BUTTON_PIN PIN_BUTTON1 + +#define PIN_BUTTON2 (12) +#define BUTTON_PIN2 PIN_BUTTON2 + +#define PIN_USER_BTN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_ANT_SW (23) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +//////////////////////////////////////////////////////////////////////////////// +// SPI1 +#define PIN_SPI1_NSS (30) +#define PIN_SPI1_SCK (31) +#define PIN_SPI1_MOSI (29) +#define PIN_SPI1_MISO (-1) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +//////////////////////////////////////////////////////////////////////////////// +// QSPI +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI +#define PIN_QSPI_SCK (46) +#define PIN_QSPI_CS (47) +#define PIN_QSPI_IO0 (44) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (45) // MISO if using two bit interface +#define PIN_QSPI_IO2 (7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (5) // HOLD if using two bit interface (i.e. not used) + +//////////////////////////////////////////////////////////////////////////////// +// Display + +#define DISP_MISO PIN_SPI1_MISO +#define DISP_MOSI PIN_SPI1_MOSI +#define DISP_SCLK PIN_SPI1_SCK +#define PIN_DISPLAY_CS PIN_SPI1_NSS +#define PIN_DISPLAY_DC (28) +#define PIN_DISPLAY_RST (2) +#define PIN_DISPLAY_BUSY (3) +#define DISP_BACKLIGHT (43) +#define DISP_EN (42) + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define PIN_GPS_RX (36) +#define PIN_GPS_TX (34) +#define PIN_GPS_EN (16) +#define PIN_GPS_RESET (-1) +#define PIN_GPS_PPS (14) +#define PIN_GPS_STANDBY (15) // Low = sleep +#define GPS_BAUD_RATE 115200 From fc26cae198cbfb6028bc9c53c52e93bd2afecd07 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:02 +0200 Subject: [PATCH 003/127] Update LocationProvider.h --- src/helpers/sensors/LocationProvider.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652e..a70ae8ca6 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -1,6 +1,8 @@ #pragma once #include "Mesh.h" +#include +#include class LocationProvider { @@ -22,4 +24,14 @@ public: virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + + // Format compact diagnostics in the caller-provided buffer. Providers that + // have more information (for example UART counters) can override this. + virtual void formatDiagnostics(char* out, size_t out_size) { + if (out_size == 0) return; + snprintf(out, out_size, "en:%u sat:%ld fix:%u", + isEnabled() ? 1U : 0U, + satellitesCount(), + isValid() ? 1U : 0U); + } }; From dcd601135e9a4439bcb647c29efaae023ba8ac23 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:31 +0200 Subject: [PATCH 004/127] Update MicroNMEALocationProvider.h --- .../sensors/MicroNMEALocationProvider.h | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb1145..0a6ebc1da 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef GPS_EN #ifdef PIN_GPS_EN @@ -51,6 +52,32 @@ class MicroNMEALocationProvider : public LocationProvider { unsigned long _last_time_sync = 0; static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes + // Observation-only diagnostics. These fields do not change GPS power, + // timing, reset behaviour, or parser input. + uint32_t _uart_bytes = 0; + uint32_t _nmea_checksum_ok = 0; + uint32_t _nmea_checksum_bad = 0; + uint32_t _begin_calls = 0; + uint32_t _stop_calls = 0; + unsigned long _last_uart_ms = 0; + unsigned long _last_nmea_ms = 0; + unsigned long _last_fix_ms = 0; + bool _uart_seen = false; + bool _nmea_seen = false; + bool _fix_seen = false; + + static uint32_t ageMs(unsigned long timestamp) { + return (uint32_t)(millis() - timestamp); + } + + static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) { + if (!seen) { + snprintf(out, out_size, "never"); + } else { + snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp)); + } + } + public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) { @@ -76,6 +103,7 @@ public : } void begin() override { + _begin_calls++; claim(); if (_pin_en != -1) { digitalWrite(_pin_en, GPS_EN_ACTIVE); @@ -94,6 +122,7 @@ public : } void stop() override { + _stop_calls++; if (_pin_en != -1) { digitalWrite(_pin_en, !GPS_EN_ACTIVE); } @@ -137,10 +166,36 @@ public : while (_gps_serial->available()) { char c = _gps_serial->read(); + _uart_bytes++; + _last_uart_ms = millis(); + _uart_seen = true; #ifdef GPS_NMEA_DEBUG Serial.print(c); #endif - nmea.process(c); + bool parsed = nmea.process(c); + + // MicroNMEA leaves the completed sentence in its buffer after the + // first line terminator. CRLF therefore counts once: CR observes the + // sentence, then LF clears the now-empty buffer. + if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') { + if (MicroNMEA::testChecksum(nmea.getSentence())) { + _nmea_checksum_ok++; + _last_nmea_ms = millis(); + _nmea_seen = true; + + // Refresh fix age only when a newly parsed GGA/RMC sentence + // carries a valid fix. Other valid NMEA sentences must not + // make an old position look fresh. + const char* message_id = nmea.getMessageID(); + if (parsed && isValid() && + (strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) { + _last_fix_ms = millis(); + _fix_seen = true; + } + } else { + _nmea_checksum_bad++; + } + } } if (!isValid()) time_valid = 0; @@ -163,4 +218,31 @@ public : } } } + + void formatDiagnostics(char* out, size_t out_size) override { + if (out_size == 0) return; + + char uart_age[11]; + char nmea_age[11]; + char fix_age[11]; + formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms); + formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms); + formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms); + + // Compact enough for CommonCLI's 160-byte reply buffer, including the + // "req:" prefix added by CommonCLI. + snprintf(out, out_size, + "en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu", + isEnabled() ? 1U : 0U, + (unsigned long)_uart_bytes, + uart_age, + (unsigned long)_nmea_checksum_ok, + (unsigned long)_nmea_checksum_bad, + nmea_age, + satellitesCount(), + isValid() ? 1U : 0U, + fix_age, + (unsigned long)_begin_calls, + (unsigned long)_stop_calls); + } }; From f110ce735c639425e1eef8c5f65906e0d44a03d2 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:58 +0200 Subject: [PATCH 005/127] Update CommonCLI.cpp --- src/helpers/CommonCLI.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e..b8fd6d567 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -344,6 +344,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "gps provider not found"); } + } else if (strcmp(command, "gps diag") == 0) { + LocationProvider * l = _sensors->getLocationProvider(); + if (l != NULL) { + const char* setting = _sensors->getSettingByKey("gps"); + bool requested = setting != NULL && strcmp(setting, "1") == 0; + int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U); + if (prefix_len < 0 || prefix_len >= 160) { + reply[159] = 0; + } else { + l->formatDiagnostics(reply + prefix_len, 160 - prefix_len); + } + } else { + strcpy(reply, "gps provider not found"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; From 890a2e2cffa98dea3ca88db3c3aba72ef23ff3f7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 14 Aug 2026 16:30:12 +1000 Subject: [PATCH 006/127] * commenting out the load/save of the fem_ properties, until they can be set --- examples/companion_radio/NodePrefs.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 453f28d5d..21766de82 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -53,8 +53,12 @@ private: //def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); + #if 0 + // NOTE: these cannot be set (yet) so don't load/save until we can. + // also, fem_rxgain WAS mapped to wrong JSON property previously def("fem_rxgain", _parent->radio_fem_rxgain); def("fem_txgain", _parent->radio_fem_txgain); + #endif def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); From e78bff0041393e508f37a4fa45ffd46f5bcb4f90 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 14 Aug 2026 16:42:54 +1000 Subject: [PATCH 007/127] * unit test no longer valid --- test/test_companion_node_prefs/test_companion_node_prefs.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index 6d3cebdfe..433c97106 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -50,6 +50,8 @@ public: const std::string& text() const { return _text; } }; +#if 0 +// Re-enable test once we can SET fem_ values in companion TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { NodePrefs saved; saved.rx_boosted_gain = 0; @@ -73,6 +75,7 @@ TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { EXPECT_EQ(0, loaded.radio_fem_rxgain); EXPECT_EQ(1, loaded.radio_fem_txgain); } +#endif int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From fcb9bdf2095f4f39d8ed433e1fb8df4278a4880f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 14 Aug 2026 17:46:43 +1000 Subject: [PATCH 008/127] * combine radio's entropy with CC310 RNG --- src/helpers/radiolib/RadioLibWrappers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 5db1e41f0..77dd93116 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -92,8 +92,10 @@ public: void random(uint8_t* dest, size_t sz) override { #ifdef USE_CC310_HW_CRYPTO - // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. nRFCrypto.Random.generate(dest, (uint16_t)sz); + for (int i = 0; i < sz; i++) { + dest[i] ^= _radio->randomByte() ^ (::random(0, 256) & 0xFF); // combine with Radio's entropy + } #else for (int i = 0; i < sz; i++) { dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF); From d92964352441e53b93e8667b802e04f6e072b39e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 14 Aug 2026 22:19:19 +1000 Subject: [PATCH 009/127] * version 1.17.1 --- examples/companion_radio/MyMesh.h | 4 ++-- examples/simple_repeater/MyMesh.h | 4 ++-- examples/simple_room_server/MyMesh.h | 4 ++-- examples/simple_sensor/SensorMesh.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f73fe0e06..238adada9 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 13 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "9 Aug 2026" +#define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.17.0" +#define FIRMWARE_VERSION "v1.17.1" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6d9cf459c..04bd4fb92 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -71,11 +71,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index ba9f7eeca..5cf949c6b 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -28,11 +28,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 3a33b638c..b5e96d5cc 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -34,11 +34,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "9 Aug 2026" + #define FIRMWARE_BUILD_DATE "14 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.0" + #define FIRMWARE_VERSION "v1.17.1" #endif #define FIRMWARE_ROLE "sensor" From 114093ee0e8fd34ca7d7434e8cb8523bd6e1ba9d Mon Sep 17 00:00:00 2001 From: Fedor Kallay Date: Sat, 15 Aug 2026 02:13:11 +0200 Subject: [PATCH 010/127] fix(lr2021): setTxPower() could leave the receiver down until reboot RadioLibWrapper::setTxPower() called setOutputPower() and nothing else. On LR2021 that writes the PA config and TxParams, which are standby-only commands, and it never re-arms the receiver. This only affects LR2021 boards. recvRaw() has: #if defined(USE_LR2021) state = STATE_RX; // LR2021 stays in Rx after readData #else state = STATE_IDLE; // need another startReceive() #endif so on SX126x the next recvRaw() re-arms Rx anyway and the write is harmless, while on LR2021 startReceive() is never called again on its own. A 'set tx' issued while the radio was listening could therefore stop reception until the next reboot. Observed a few times on an LR2021 repeater; recovery required a power cycle. Dispatcher's stuck-radio check does not catch it: isInRecvMode() reports the wrapper's own `state` flag rather than the chip's actual mode, so it still believes the radio is in Rx, and it only raises ERR_EVENT_STARTRX_TIMEOUT without attempting recovery. Fix: on LR2021, drop to standby via idle() before writing the PA config and let checkRecv() re-arm Rx - the same pattern resetAGC() and applySideDetectorConfig() already use. Other radios are left untouched. Affects meshtracker_x1 and meshnology_w12 (both USE_LR2021). Tested on hardware with an LR2021 repeater: six consecutive 'set tx' changes (15/18/21/14/19/14), after which the radio kept receiving and forwarding traffic (rawrx/rxpkts increasing, no missed IRQs). Noise-floor sampling also kept updating, which only happens while state == STATE_RX. --- src/helpers/radiolib/RadioLibWrappers.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b9c095ac4..f404b6257 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -48,6 +48,17 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { +#if defined(USE_LR2021) + // On LR2021, setOutputPower() writes the PA config and TxParams, which are + // standby-only commands. recvRaw() keeps state == STATE_RX after readData on + // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which + // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls + // startReceive() again by itself. Writing the PA config while listening could + // therefore leave the receiver down until the next reboot. Drop to standby + // first and let checkRecv() re-arm Rx, as resetAGC() and + // applySideDetectorConfig() already do. + idle(); +#endif _radio->setOutputPower(dbm); } From 0a595ae79db209add0c1addffbae2626ea725cb7 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 16 Aug 2026 11:40:13 -0400 Subject: [PATCH 011/127] gxepddisplay: better handling of full refresh (no faint partial afterwards !!!) --- src/helpers/ui/GxEPDDisplay.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 11f78b570..610b8541f 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -88,7 +88,7 @@ void GxEPDDisplay::turnOff() { display.fillScreen(GxEPD_WHITE); display.display(true); forceFullRefresh(); - display.powerOff(); + display.hibernate(); } void GxEPDDisplay::clear() { @@ -101,11 +101,15 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); - if (_cycles_before_full_refresh <= 0) { + if (_cycles_before_full_refresh != 0) { display.setPartialWindow(0, 0, display.width(), display.height()); } else { - display.setFullWindow(); - display.writeScreenBuffer(); + // forces a full wipe of the screen ... + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + // we'll need a partial refresh after that (whatever crc value is) + last_display_crc_value = 0; + resetPartialRefreshCounter(); } } @@ -211,16 +215,10 @@ void GxEPDDisplay::endFrame() { if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - if (_cycles_before_full_refresh == 0) { - display.display(false); - display.writeScreenBuffer(); - resetPartialRefreshCounter(); - } else { - display.display(true); - if (_cycles_before_full_refresh > 0) { - _cycles_before_full_refresh--; - } + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; } - last_display_crc_value = crc; } + last_display_crc_value = crc; } From a8a60015769c712b3af25cd6c671cb0c9c706698 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 012/127] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb92..cac6c4a28 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068..e06544647 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 9387f55d7e3637becce6e30d6472be3a06da145f Mon Sep 17 00:00:00 2001 From: Huw Duddy <37787853+oltaco@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:16:01 +1000 Subject: [PATCH 013/127] trimmed comment --- src/helpers/radiolib/RadioLibWrappers.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index f404b6257..e4d2ba1c2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -49,14 +49,6 @@ uint32_t RadioLibWrapper::getRngSeed() { void RadioLibWrapper::setTxPower(int8_t dbm) { #if defined(USE_LR2021) - // On LR2021, setOutputPower() writes the PA config and TxParams, which are - // standby-only commands. recvRaw() keeps state == STATE_RX after readData on - // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which - // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls - // startReceive() again by itself. Writing the PA config while listening could - // therefore leave the receiver down until the next reboot. Drop to standby - // first and let checkRecv() re-arm Rx, as resetAGC() and - // applySideDetectorConfig() already do. idle(); #endif _radio->setOutputPower(dbm); @@ -265,4 +257,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +} From de0d5031decd44f1d8659d58db89e6c823408755 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:24:17 -0400 Subject: [PATCH 014/127] M8: preview messages up to 164 cars --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- variants/thinknode_m8/platformio.ini | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d5e95d157..59b58461e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -511,6 +511,10 @@ public: } }; +#ifndef UI_MSG_PREVIEW_SIZE + #define UI_MSG_PREVIEW_SIZE 78 +#endif + class MsgPreviewScreen : public UIScreen { UITask* _task; mesh::RTCClock* _rtc; @@ -518,7 +522,7 @@ class MsgPreviewScreen : public UIScreen { struct MsgEntry { uint32_t timestamp; char origin[62]; - char msg[78]; + char msg[UI_MSG_PREVIEW_SIZE]; }; #define MAX_UNREAD_MSGS 32 int num_unread; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 9f1595c55..b8c709739 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -23,6 +23,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 -D UI_TZ_OFFSET=-4 # GMT-4 + -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 From 7f664f100dad6e99657ef409fe3f3c873a507316 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:41:51 -0400 Subject: [PATCH 015/127] gxepd: do full refresh before screen off ... so wakes quicker ;) --- src/helpers/ui/GxEPDDisplay.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 610b8541f..332157eb8 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -71,9 +71,7 @@ void GxEPDDisplay::turnOn() { expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif if (!_isOn) { - forceFullRefresh(); _isOn = true; - last_display_crc_value=0; } } @@ -84,10 +82,12 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; - display.setFullWindow(); - display.fillScreen(GxEPD_WHITE); - display.display(true); - forceFullRefresh(); + // do full refresh before powering off to clear screen + // no full refresh needed at wakeup + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + resetPartialRefreshCounter(); + last_display_crc_value=0; display.hibernate(); } From 2b0ed0000494a228536328fa19ddbcb4e774e26e Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:34:19 -0700 Subject: [PATCH 016/127] fix(sensors): claim an I2C address after a successful init Several table entries share an address and not every driver verifies a chip ID: INA226::begin() only checks that the address ACKs, so an SHT4x at 0x44 was also registered as an INA226 and reported junk current on a second channel. Mark the address consumed once a driver initializes it so later entries cannot re-claim the same device. --- src/helpers/sensors/EnvironmentSensorManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index e2f0d33e7..c3dfd9f75 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -650,6 +650,7 @@ bool EnvironmentSensorManager::begin() { continue; } MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address); + detected[def.address] = false; // consumed; later entries must not re-claim this device for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) { _active_sensors[_active_sensor_count++] = { def.query, sub }; } From 2a6eabe12012f06a7f1a9ae06c25c50161e67ecf Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:59:59 -0700 Subject: [PATCH 017/127] fix(sensors): probe BMP/BME at both 0x76 and 0x77 Grove and other Bosch modules strap SDO high, so the 0x76-only table never initialized them. Add an alternate-address entry per Bosch sensor; the bus scan still gates every probe, and all four drivers verify a chip ID before claiming an address. Each sensor type has a single static driver instance, so skip an entry whose query is already active: the alternate address is a fallback, not a second device. --- .../sensors/EnvironmentSensorManager.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index c3dfd9f75..543056927 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -539,6 +539,8 @@ static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) { // are compiled in. The sentinel at the end keeps the array // non-empty regardless of which sensors are enabled. // +// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both. +// // Ordering here determines channel assignment at runtime: // the first detected+initialized sensor gets channel 2, the // next gets channel 3, and so on. @@ -551,21 +553,27 @@ struct SensorDef { void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry); }; +#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76)) + static const SensorDef SENSOR_TABLE[] = { #if ENV_INCLUDE_AHTX0 { TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 }, #endif #ifdef ENV_INCLUDE_BME680 - { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 }, #endif #if ENV_INCLUDE_BME680_BSEC - { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, #endif #if ENV_INCLUDE_BME280 - { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 }, #endif #if ENV_INCLUDE_BMP280 - { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 }, #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, @@ -603,6 +611,8 @@ static const SensorDef SENSOR_TABLE[] = { { 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty }; +#undef TELEM_BOSCH_ALT_ADDR + static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1; // ============================================================ @@ -640,6 +650,12 @@ bool EnvironmentSensorManager::begin() { _active_sensor_count = 0; for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) { const SensorDef& def = SENSOR_TABLE[i]; + // One static driver instance per type: an alternate address is a fallback, not a second device. + bool already_active = false; + for (int j = 0; j < _active_sensor_count; j++) { + if (_active_sensors[j].query == def.query) { already_active = true; break; } + } + if (already_active) continue; if (!detected[def.address]) { MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address); continue; From 0cce9197a18572d484a284058a896c8d1a7cc0bb Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 18 Aug 2026 13:11:15 +1000 Subject: [PATCH 018/127] Fix for CMD_SEND_RAW_DATA and multi-byte paths --- examples/companion_radio/MyMesh.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c3340663..16603bc4a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1512,15 +1512,19 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); + if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { + uint8_t path[MAX_PATH_SIZE]; + i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); + if (i + 4 > len) { // min payload 4 bytes + writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else { - writeErrFrame(ERR_CODE_TABLE_FULL); + auto pkt = createRawData(&cmd_frame[i], len - i); + if (pkt) { + sendDirect(pkt, path, path_len); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) From 38f10cd12b0c766db9599973b2fc751da29af908 Mon Sep 17 00:00:00 2001 From: Dominik Tyrala Date: Mon, 17 Aug 2026 15:50:46 +0200 Subject: [PATCH 019/127] Fixes BLE exceeding flash size --- variants/xiao_c3/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 587c5c273..c9c107c68 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -73,6 +73,7 @@ lib_deps = [env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + From d65da5a81a5385a10d99212ad1be0f8417f0b593 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 13:51:55 +0200 Subject: [PATCH 020/127] fix: move MeshadventurerBoard.h to correct location --- {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h | 2 +- variants/meshadventurer/target.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h (97%) diff --git a/src/helpers/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h similarity index 97% rename from src/helpers/MeshadventurerBoard.h rename to variants/meshadventurer/MeshadventurerBoard.h index 0325161d5..d9e88e1bc 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -13,7 +13,7 @@ #define PIN_VBAT_READ 35 -#include "ESP32Board.h" +#include "helpers/ESP32Board.h" class MeshadventurerBoard : public ESP32Board { diff --git a/variants/meshadventurer/target.h b/variants/meshadventurer/target.h index d59b3aee8..0749565d3 100644 --- a/variants/meshadventurer/target.h +++ b/variants/meshadventurer/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -44,4 +44,3 @@ extern MASensorManager sensors; bool radio_init(); mesh::LocalIdentity radio_new_identity(); - From 2db4b40617c3d285342d7f418be254ebbfead75b Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 14:56:27 +0200 Subject: [PATCH 021/127] repeater: set `loop.detect` to `minimal` as default for new installs --- examples/simple_repeater/MyMesh.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3a..a711ec0a5 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t int results_offset = 0; uint8_t results_buffer[130]; for(int index = 0; index < count && index + offset < neighbours_count; index++){ - + // stop if we can't fit another entry in results int entry_size = pubkey_prefix_length + 4 + 1; if(results_offset + entry_size > sizeof(results_buffer)){ @@ -907,6 +907,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.loop_detect = LOOP_DETECT_MINIMAL; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -1170,7 +1171,7 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } From 612b217b90a52ebd1c88218b25584fac82da62c0 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 12:07:29 +1200 Subject: [PATCH 022/127] updated gcc toolchain for nrf52 boards --- platformio.ini | 1 + src/armstubs.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 src/armstubs.cpp diff --git a/platformio.ini b/platformio.ini index e78124a40..ee502473c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -86,6 +86,7 @@ platform_packages = ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM diff --git a/src/armstubs.cpp b/src/armstubs.cpp new file mode 100644 index 000000000..30da40ecf --- /dev/null +++ b/src/armstubs.cpp @@ -0,0 +1,9 @@ +// https://github.com/meshcore-dev/MeshCore/issues/2469 +// armstubs.cpp exists to silence warnings introduced after upgrading platformio/toolchain-gccarmnoneeabi +extern "C" __attribute__((weak)) int _close(int fd) { return -1; } +extern "C" __attribute__((weak)) int _lseek(int fd, int offset, int whence) { return 0; } +extern "C" __attribute__((weak)) int _read(int fd, char *buf, int len) { return 0; } +extern "C" __attribute__((weak)) int _fstat(int fd, struct stat *st) { return 0; } +extern "C" __attribute__((weak)) int _isatty(int fd) { return 1; } +extern "C" __attribute__((weak)) int _getpid(void) { return 1; } +extern "C" __attribute__((weak)) int _kill(int pid, int sig) { return -1; } From 4b8966d4f9496cc6b5b83c434bb6d7e5027cdb8b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 19 Aug 2026 15:29:19 +1000 Subject: [PATCH 023/127] WIO Tracker L1 1W variant --- examples/companion_radio/MyMesh.cpp | 4 + .../wio-tracker-l1-1w/WioTrackerL1Board.cpp | 40 ++++++ .../wio-tracker-l1-1w/WioTrackerL1Board.h | 46 +++++++ variants/wio-tracker-l1-1w/platformio.ini | 121 ++++++++++++++++ variants/wio-tracker-l1-1w/target.cpp | 40 ++++++ variants/wio-tracker-l1-1w/target.h | 35 +++++ variants/wio-tracker-l1-1w/variant.cpp | 78 +++++++++++ variants/wio-tracker-l1-1w/variant.h | 130 ++++++++++++++++++ 8 files changed, 494 insertions(+) create mode 100644 variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp create mode 100644 variants/wio-tracker-l1-1w/WioTrackerL1Board.h create mode 100644 variants/wio-tracker-l1-1w/platformio.ini create mode 100644 variants/wio-tracker-l1-1w/target.cpp create mode 100644 variants/wio-tracker-l1-1w/target.h create mode 100644 variants/wio-tracker-l1-1w/variant.cpp create mode 100644 variants/wio-tracker-l1-1w/variant.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16603bc4a..a6739ac09 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -886,7 +886,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default _prefs.radio_fem_rxgain = 1; +#ifdef RADIO_FEM_TXGAIN + _prefs.radio_fem_txgain = RADIO_FEM_TXGAIN; // board-specific default for PA +#else _prefs.radio_fem_txgain = 0; +#endif //_prefs.rx_delay_base = 10.0f; enable once new algo fixed _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp new file mode 100644 index 000000000..8210c24ed --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include "WioTrackerL1Board.h" + +void WioTrackerL1Board::begin() { + NRF52BoardDCDC::begin(); + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input + // Set all button pins to INPUT_PULLUP + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { + _is_pa_enabled = enable; + digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA + return true; +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h new file mode 100644 index 000000000..c5a609443 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +class WioTrackerL1Board : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + bool _is_pa_enabled = false; + +public: + WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + void begin(); + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + bool canControlLoRaFemPaGain() const override { return true; } + bool setLoRaFemPaGainEnabled(bool enable) override; + bool isLoRaFemPaGainEnabled() const override { return _is_pa_enabled; } + + void powerOff() override { + setLoRaFemPaGainEnabled(false); // turn Off PA + NRF52Board::powerOff(); + } +}; diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini new file mode 100644 index 000000000..e5b4a3be3 --- /dev/null +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -0,0 +1,121 @@ +[WioTrackerL1-1W] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1-1w + -D WIO_TRACKER_L1 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_OLED_RESET=-1 + -D GPS_BAUD_RATE=9600 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1-1w> + + + + +lib_deps= ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + +[env:WioTrackerL1-1W_repeater] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_room_server] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_companion_radio_usb] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ENABLE_USB_INTERFACE +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + + + +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_companion_radio_ble] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ADVERT_NAME='"@@MAC"' + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_kiss_modem] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/wio-tracker-l1-1w/target.cpp b/variants/wio-tracker-l1-1w/target.cpp new file mode 100644 index 000000000..7a573258e --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.cpp @@ -0,0 +1,40 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/wio-tracker-l1-1w/target.h b/variants/wio-tracker-l1-1w/target.h new file mode 100644 index 000000000..05bc7ff1c --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.h @@ -0,0 +1,35 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #if defined(WIO_TRACKER_L1_EINK) + #include + #else + #include + #endif + #include +#endif +#include + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/wio-tracker-l1-1w/variant.cpp b/variants/wio-tracker-l1-1w/variant.cpp new file mode 100644 index 000000000..18efe7eaf --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.cpp @@ -0,0 +1,78 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 29, // D5 P0.29 (AIN5) LORA_VDET 鈥?Pro 1W: P0.29 (not P1.08) + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.01 Mesh_LED (orange) 鈥?Pro 1W: P1.01 (not P1.15) + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL + + // D31-D33 - Pro 1W only + 13, // D31 P0.13 BOOST_EN (Grove 5V Boost) + 47, // D32 P1.15 nRF_Sig_Charge_State (BQ25616 STAT) + 14, // D33 P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO) +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1-1w/variant.h b/variants/wio-tracker-l1-1w/variant.h new file mode 100644 index 000000000..7520460ab --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.h @@ -0,0 +1,130 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (34) +#define NUM_DIGITAL_PINS (34) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (29) // Joystick Press +#define PIN_BACK_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 +#define PIN_USER_BTN PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// BQ25616 single-wire charge status (Pro 1W) +#define PIN_BOOST_EN (31) // D31 / P0.13 鈥?Grove 5V Boost enable +#define EXT_CHRG_DETECT (32) // D32 / P1.15 鈥?BQ25616 STAT +#define EXT_CHRG_DETECT_VALUE LOW // 0 = charging, 1 = full / charger sleep +#define BOOST_EN_ACTIVE HIGH // HIGH enables Grove 5V Boost + +// Lora Pins +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_DIO_1 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) + +// LORA_PWR_EN (P0.14): external LDO enable for the SX1262 + 1 W PA on the Pro 1W board. +#define LORA_PWR_EN (33) +#define SX126X_POWER_EN LORA_PWR_EN + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (18) +#define PIN_WIRE1_SCL (17) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (PIN_GPS_STANDBY) + +// QSPI Pins +#define PIN_QSPI_SCK (19) +#define PIN_QSPI_CS (20) +#define PIN_QSPI_IO0 (21) +#define PIN_QSPI_IO1 (22) +#define PIN_QSPI_IO2 (23) +#define PIN_QSPI_IO3 (24) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// // EInk on SPI1 +// #define PIN_DISPLAY_CS (36) +// #define PIN_DISPLAY_BUSY (35) +// #define PIN_DISPLAY_DC (34) +// #define PIN_DISPLAY_RST (32) + +#define PIN_SPI1_MISO (37) +#define PIN_SPI1_MOSI (33) +#define PIN_SPI1_SCK (31) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +#endif \ No newline at end of file From 67d6d42a4c1cc57196e00817a2c2271dffe24cc6 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 18:59:24 +1200 Subject: [PATCH 024/127] fix for 3-byte paths passed to CMD_SEND_RAW_DATA --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16603bc4a..fdece4829 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1511,7 +1511,7 @@ void MyMesh::handleCmdFrame(size_t len) { #endif } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; - int8_t path_len = cmd_frame[i++]; + uint8_t path_len = cmd_frame[i++]; if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { uint8_t path[MAX_PATH_SIZE]; i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); From b76242043399cd5a3a4bfe1e36b505a69165ab25 Mon Sep 17 00:00:00 2001 From: Thomas Osterried Date: Wed, 19 Aug 2026 09:35:25 +0200 Subject: [PATCH 025/127] Skip empty channel slots in searchChannelsByHash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unconfigured slot has an all-zero secret, so it matches null-key group traffic (a sender with an unset PSK). The zero-key MAC validates against the empty slot and the foreign message is delivered as if it belonged to that channel — every node with a free slot is a null-key sink. Skip empty slots. --- src/helpers/BaseChatMesh.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9e..616ee39bb 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -368,6 +368,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8 int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Skip empty/unconfigured slots. An empty slot has an all-zero secret and + // therefore matches null-key group traffic (a node transmitting with an + // unset PSK): the zero-key MAC validates against the empty slot and the + // foreign message is delivered as if it belonged to that channel. Any node + // with a free channel slot would otherwise act as a null-key sink. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } From 3a440ac41af0e19f2f79aba7b5efd16a80988963 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:06:43 -0400 Subject: [PATCH 026/127] Add charging indicator to companion_radio battery icon Show a small lightning-bolt icon to the left of the battery indicator on the ui-new home screen while the device is externally powered, and a plug icon once the battery reads full. The bolt/plug sits beside the battery so the fill bar stays clean and uninterrupted. When a buzzer is present, the mute icon shifts one slot further left so the two never overlap. Charging state is derived from board.isExternalPowered(), so this works on any board that reports external power (e.g. the nRF52 VBUS-detect path). Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 14 ++++++++++++-- examples/companion_radio/ui-new/icons.h | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 59b58461e..617f5e874 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -146,11 +146,21 @@ class HomeScreen : public UIScreen { int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); - // show muted icon if buzzer is muted + // while charging, show a bolt (or a plug once full) just left of the battery, + // keeping the fill bar itself clean and uninterrupted + bool charging = board.isExternalPowered(); + if (charging) { + const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + display.setColor(UIColor::title_txt); + display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); + } + + // show muted icon if buzzer is muted (shifted further left when the charging + // icon already occupies the slot immediately left of the battery) #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { display.setColor(UIColor::warning_txt); - display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8); } #endif } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe237902..8d9ab175f 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 +}; + +// small lightning bolt, 8x8px, shown next to the battery icon while charging +static const uint8_t charging_icon[] = { + 0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0 +}; + +// small power plug, 8x8px, shown next to the battery icon once fully charged +static const uint8_t plug_icon[] = { + 0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18 }; \ No newline at end of file From 41c60da5759ccb2c0a04e3b7a354e66e51c2de03 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:14:12 -0400 Subject: [PATCH 027/127] Show plug at >=95% instead of exactly 100% Boards without a charge-complete signal only infer "full" from voltage, and a real pack rarely reads the full BATT_MAX_MILLIVOLTS (4.2V), so the plug icon was effectively never shown. Treat "full" as a high band (>= 95%) so the plug appears when the battery is charged rather than requiring an exact 100%. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 617f5e874..55755ef17 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -150,7 +150,10 @@ class HomeScreen : public UIScreen { // keeping the fill bar itself clean and uninterrupted bool charging = board.isExternalPowered(); if (charging) { - const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + // There's no charge-complete signal on most boards, so "full" is a high + // voltage band rather than an exact 100% (a real pack rarely reads 4.2V). + const int BATT_FULL_PCT = 95; + const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon; display.setColor(UIColor::title_txt); display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); } From afb969ccca2f06913aec207a3cd4f2910806fabc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 21 Aug 2026 20:55:57 +1000 Subject: [PATCH 028/127] * initial wiring/routing of CLI commands to companion (either via app/serial interface, or remotely via txt message) --- examples/companion_radio/MyMesh.cpp | 57 ++++++++++++++++++++++------ examples/companion_radio/MyMesh.h | 6 ++- examples/simple_secure_chat/main.cpp | 2 +- src/Utils.cpp | 9 +++++ src/Utils.h | 2 + src/helpers/BaseChatMesh.cpp | 43 +++++++++++++++------ src/helpers/BaseChatMesh.h | 2 +- src/helpers/ContactInfo.h | 6 +++ 8 files changed, 101 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fdece4829..49c9f91b0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,6 +62,7 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 +#define CMD_RUN_CLI_COMMAND 66 // v14+ // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -97,6 +98,7 @@ #define RESP_ALLOWED_REPEAT_FREQ 26 #define RESP_CODE_CHANNEL_DATA_RECV 27 #define RESP_CODE_DEFAULT_FLOOD_SCOPE 28 +#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND #define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9) @@ -529,9 +531,13 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t } void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) { + const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + if (from.isRemoteCLIAllowed() && handleCommand(text, sender_timestamp, reply)) { + // CLI command was handled. Let BaseChatMesh handle the sending of the reply + } else { + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + } } void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, @@ -1082,6 +1088,21 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], _prefs.node_name, tlen); i += tlen; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+ + int i = 1; + char *text = (char *)&cmd_frame[i]; + int tlen = len - i; + text[tlen] = 0; // ensure null + + reply_buf[0] = 0; + if (handleCommand(text, 0, reply_buf)) { + out_frame[0] = RESP_CODE_CLI_REPLY; + int rlen = strlen(reply_buf); + memcpy(&out_frame[1], reply_buf, rlen); + _serial->writeFrame(out_frame, 1 + rlen); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // unsupported command + } } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { int i = 1; uint8_t txt_type = cmd_frame[i++]; @@ -2031,6 +2052,25 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } +bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + *reply = 0; + command += 3; + } + + if (memcmp(command, "set pin ", 8) == 0) { + _prefs.ble_pin = atoi(&command[8]); + savePrefs(); + sprintf(reply, "> pin is now %06d", _prefs.ble_pin); + return true; + } + return false; // not handled +} + void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { @@ -2048,15 +2088,10 @@ void MyMesh::checkCLIRescueCmd() { if (len > 0 && cli_command[len - 1] == '\r') { // received complete line cli_command[len - 1] = 0; // replace newline with C string null terminator - if (memcmp(cli_command, "set ", 4) == 0) { - const char* config = &cli_command[4]; - if (memcmp(config, "pin ", 4) == 0) { - _prefs.ble_pin = atoi(&config[4]); - savePrefs(); - Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); - } else { - Serial.printf(" Error: unknown config: %s\n", config); - } + reply_buf[0] = 0; + if (handleCommand(cli_command, 0, reply_buf)) { + // command was handled, print reply output + Serial.print(" "); Serial.print(reply_buf); Serial.println(); } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada9..9c479c150 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 13 +#define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "14 Aug 2026" @@ -134,7 +134,7 @@ protected: void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) override; + const char *text, char* reply) override; void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, @@ -201,6 +201,7 @@ private: } void checkCLIRescueCmd(); + bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; @@ -225,6 +226,7 @@ private: bool _cli_rescue; bool send_unscoped; // force un-scoped flood (instead of using send_scope) char cli_command[80]; + char reply_buf[166]; uint8_t app_target_ver; uint8_t *sign_data; uint32_t sign_data_len; diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da42ddcbb..241fe1c21 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -240,7 +240,7 @@ protected: } } - void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { + void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { } void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { } diff --git a/src/Utils.cpp b/src/Utils.cpp index 5ae7f0e27..7a3fb78b3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -203,6 +203,15 @@ bool Utils::isHexChar(char c) { return c == '0' || hexVal(c) > 0; } +bool Utils::isZeroes(const uint8_t* buf, size_t len) { + while (len > 0) { + if (*buf != 0) return false; + buf++; + len--; + } + return true; +} + bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) { int len = strlen(src_hex); if (len != dest_size*2) return false; // incorrect length diff --git a/src/Utils.h b/src/Utils.h index 5736b8747..7a0f7b6ee 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -82,6 +82,8 @@ public: static int parseTextParts(char* text, const char* parts[], int max_num, char separator=','); static bool isHexChar(char c); + + static bool isZeroes(const uint8_t* buf, size_t len); }; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 616ee39bb..bf8a861c9 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,6 +9,8 @@ #define TXT_ACK_DELAY 200 #endif +#define CLI_REPLY_DELAY_MILLIS 600 + void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { sendFlood(pkt, delay_millis); } @@ -227,8 +229,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender ContactInfo& from = contacts[i]; if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { - uint32_t timestamp; - memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = data[4] >> 2; // message attempt number, and other flags // len can be > original length, but 'text' will be padded with zeroes @@ -236,7 +238,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (flags == TXT_TYPE_PLAIN) { from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know int text_len = strlen((char *)&data[5]); uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it @@ -254,20 +256,39 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { - onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + uint8_t temp[166]; + char *command = (char *)&data[5]; + char *reply = (char *)&temp[5]; + *reply = 0; + + onCommandDataRecv(from, packet, sender_timestamp, command, reply); // let UI know // NOTE: no ack expected for CLI_DATA replies - if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) - mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0); - if (path) sendFloodScoped(from, path); + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + timestamp++; + } + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len); + if (reply_pkt) { + if (from.out_path_len == OUT_PATH_UNKNOWN) { + sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { - if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date - from.sync_since = timestamp; + if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date + from.sync_since = sender_timestamp; } from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know + onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d98785470..331d1041b 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -113,7 +113,7 @@ protected: virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len); virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; - virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0; virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index ede977cac..5a156c829 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -26,6 +26,12 @@ struct ContactInfo { return shared_secret; } + bool isFav() const { return flags & 0x01; } + bool isTelemBaseAllowed() const { return flags & 0x02; } + bool isTelemLocAllowed() const { return flags & 0x04; } + bool isTelemEnvAllowed() const { return flags & 0x08; } + bool isRemoteCLIAllowed() const { return flags & 0x10; } + private: mutable uint8_t shared_secret[PUB_KEY_SIZE]; }; From e749010bbc9ab61215c3d73d3e35d60058d23929 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 029/127] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb92..cac6c4a28 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068..e06544647 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 52f0362c09aec05323803cada70f17c8efb83eee Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 20 Aug 2026 21:17:17 -0400 Subject: [PATCH 030/127] ui: repeater discover screen --- examples/companion_radio/MyMesh.cpp | 50 ++++++++++++++++++++ examples/companion_radio/MyMesh.h | 18 ++++++++ examples/companion_radio/ui-new/UITask.cpp | 53 +++++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fdece4829..01512b163 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -134,6 +134,10 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 +// Copied from simple_repeater (could probably be shared) +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -403,6 +407,51 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { + if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; + if (max_num > disc_nodes_count) max_num = disc_nodes_count; + + for (int i = 0; i < max_num; i++) { + nodes[i] = discovered_nodes[i]; + } + return max_num; +} + +bool MyMesh::requestRepeatersDiscovery() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + getRNG()->random(&cmd_bytes[2], 4); // tag + disc_nodes_count = 0; + disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); + mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + sendZeroHop(req); + return true; + } + return false; +} + +void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { + if ((p_len < 12) + || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) + || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) + || (memcmp(&payload[2], &disc_node_req_tag, 4))) { + return; + } + memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); + discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; + discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; + discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); + ContactInfo* c = lookupContactByPubKey(&payload[6], 8); + if (c != NULL) { + strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); + } else { + discovered_nodes[disc_nodes_count].name[0] = 0; + } + disc_nodes_count ++; +} + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -783,6 +832,7 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } + checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada9..848d21c05 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,14 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +struct DiscoveredNode { + uint8_t pubkey_prefix[9]; + float snr_in; + float snr_out; + char name[32]; + uint8_t type; +}; + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); @@ -102,6 +110,9 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); + bool requestRepeatersDiscovery(); + int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); + protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -256,6 +267,13 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + + #define DISCOVERED_NODES_TABLE_SIZE 10 + DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; + + void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef17..355250282 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -24,6 +24,7 @@ #define LONG_PRESS_MILLIS 1200 +// Used both for recent adverts and discovered nodes #ifndef UI_RECENT_LIST_SIZE #define UI_RECENT_LIST_SIZE 4 #endif @@ -102,6 +103,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif + DISCOVERY, SHUTDOWN, Count // keep as last }; @@ -113,7 +115,9 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - + DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; + uint32_t discovery_req_time = 0; + bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -459,6 +463,39 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif + } else if (_page == HomePage::DISCOVERY) { + int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + display.setColor(UIColor::primary_txt); + int y = 20; + for (int i = 0; i < count; i++, y += 11) { + char name[32]; + auto a = &discovered[i]; + if ((a->name[0] == 0) || !discovery_disp_names) { + mesh::Utils::toHex(name, a->pubkey_prefix, 4); + } else { + strncpy(name, a->name, 32); + } + char filtered_name[sizeof(name)]; + char snr_s[12]; + if (strlen(name) <= 8) { // display snr_o + sprintf(snr_s, "%02.1f>%02.1f", a->snr_out, a->snr_in); + } else { + sprintf(snr_s, "%02.1f", a->snr_in); + } + int snr_width = display.getTextWidth(snr_s); + int max_name_width = display.width() - snr_width - 1; + display.translateUTF8ToBlocks(filtered_name, name, sizeof(filtered_name)); + display.drawTextEllipsized(0, y, max_name_width, filtered_name); + display.setCursor(display.width() - snr_width - 1, y); + display.print(snr_s); + } + if (millis() < discovery_req_time + 5000) { + return 1000; // more frequent updates just after req + } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * UI_RECENT_LIST_SIZE; + display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); + } + } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -484,6 +521,9 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -516,6 +556,17 @@ public: return true; } #endif + if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { + if (millis() > discovery_req_time + 5000) { // rate limiter + the_mesh.requestRepeatersDiscovery(); + discovery_req_time = millis(); + } + return true; + } + if (c == KEY_SELECT && _page == HomePage::DISCOVERY) { + discovery_disp_names = !discovery_disp_names; + return true; + } if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; From 0b652b56294d2d6a23c7ca62b14b8215b1ccca4a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sat, 22 Aug 2026 16:16:32 +1200 Subject: [PATCH 031/127] add support for rak12500 and rak12501 gps on rak3401 --- variants/rak3401/platformio.ini | 1 + variants/rak3401/variant.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e719226..c285e3ec5 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 + -D RAK_BOARD -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 988278860..e0c24759b 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 From ab4c33826dbcaf80aad89ac94048acc104e553d5 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 23 Aug 2026 14:14:45 +1200 Subject: [PATCH 032/127] don't toggle io pins on rak3401 when probing gps --- src/helpers/sensors/EnvironmentSensorManager.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 543056927..6f4607751 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -831,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } #endif + #ifndef RAK_3401 //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); delay(500); digitalWrite(ioPin,HIGH); + #endif + + // give gps time to power up delay(500); //Try to init RAK12500 on I2C @@ -871,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ return true; } + #ifndef RAK_3401 pinMode(ioPin, INPUT); + #endif MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } @@ -880,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, HIGH); + #endif return; #endif @@ -896,8 +904,10 @@ void EnvironmentSensorManager::start_gps() { void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, LOW); + #endif return; #endif From 41588d805253fd4d4852354efd4f369d99ed3496 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 14:45:15 +1000 Subject: [PATCH 033/127] * adding new CommonRadioPrefs * refactor: moving various radio CLI handling to CommonRadioPrefs --- examples/companion_radio/MyMesh.cpp | 26 +++++++++++++ examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/NodePrefs.h | 18 ++++++++- src/helpers/AdvertDataHelpers.cpp | 8 ++++ src/helpers/AdvertDataHelpers.h | 2 + src/helpers/CommonCLI.cpp | 51 +++++-------------------- src/helpers/CommonCLI.h | 17 ++++++++- src/helpers/CommonRadioPrefs.cpp | 56 ++++++++++++++++++++++++++++ src/helpers/CommonRadioPrefs.h | 45 ++++++++++++++++++++++ 9 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 src/helpers/CommonRadioPrefs.cpp create mode 100644 src/helpers/CommonRadioPrefs.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 49c9f91b0..54be91c78 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2062,12 +2062,38 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* command += 3; } + if (_prefs.getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is radio CLI command? + if (_prefs.getRadioPrefs()->isDirty()) { savePrefs(); } + return true; + } + + if (memcmp(command, "set name ", 9) == 0) { + if (AdvertDataParser::isValidName(&command[9])) { + StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + return true; + } + if (strcmp(command, "get name") == 0) { + sprintf(reply, "> %s", _prefs.node_name); + return true; + } + if (memcmp(command, "set pin ", 8) == 0) { _prefs.ble_pin = atoi(&command[8]); savePrefs(); sprintf(reply, "> pin is now %06d", _prefs.ble_pin); return true; } + + if (strcmp(command, "ver") == 0) { + sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); + return true; + } + return false; // not handled } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 9c479c150..b60030a6d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -169,6 +169,7 @@ public: _prefs.node_lat = sensors.node_lat; _prefs.node_lon = sensors.node_lon; _store->savePrefs(_prefs); + _prefs.clearDirty(); } #if ENV_INCLUDE_GPS == 1 diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 21766de82..367880526 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,7 @@ #pragma once #include // For uint8_t, uint32_t #include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -42,7 +43,7 @@ public: uint8_t default_scope_key[16]; private: - class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -70,6 +71,18 @@ private: } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + + // CommonRadioPrefs interface + float getFreq() const override { return _parent->freq; } + void setFreq(float f) override { _parent->freq = f; markDirty(); } + float getBandwidth() const override { return _parent->bw; } + void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } + uint8_t getSpreadFactor() const override { return _parent->sf; } + void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + uint8_t getCodingRate() const override { return _parent->cr; } + void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + float getAirtimeFactor() const override { return _parent->airtime_factor; } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } }; RadioPrefs radio; @@ -142,4 +155,7 @@ public: // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } + + CommonRadioPrefs* getRadioPrefs() { return &radio; } + void clearDirty() { radio.clearDirty(); } }; diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 998733ae0..25e5cd3fe 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -28,6 +28,14 @@ return i; } +bool AdvertDataParser::isValidName(const char *n) { + while (*n) { + if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; + n++; + } + return true; +} + AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) { _name[0] = 0; _lat = _lon = 0; diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index abe14cbd0..f4e109e9f 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -50,6 +50,8 @@ class AdvertDataParser { public: AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len); + static bool isValidName(const char* name); + bool isValid() const { return _valid; } uint8_t getType() const { return _flags & 0x0F; } uint16_t getFeat1() const { return _extra1; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e..4e7af30b0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -164,6 +164,7 @@ void CommonCLI::savePrefs() { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } _callbacks->savePrefs(); + _prefs->clearDirty(); } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -180,6 +181,11 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { } void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + if (_prefs->getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is a radio CLI command? + if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); } + return; + } + if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { @@ -447,19 +453,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "dutycycle ", 10) == 0) { - float dc = atof(&config[10]); - if (dc < 1 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 1-100"); - } else { - _prefs->airtime_factor = (100.0f / dc) - 1.0f; - savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%%", a_int, a_frac); - } - } else if (memcmp(config, "af ", 3) == 0) { + + if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); strcpy(reply, "OK"); @@ -585,24 +580,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error: state must be on or off"); } - } else if (memcmp(config, "radio ", 6) == 0) { - strcpy(tmp, &config[6]); - const char *parts[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); - float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); @@ -806,12 +783,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "dutycycle", 9) == 0) { - float dc = 100.0f / (_prefs->airtime_factor + 1.0f); - int dc_int = (int)dc; - int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); - sprintf(reply, "> %d.%d%%", dc_int, dc_frac); - } else if (memcmp(config, "af", 2) == 0) { + if (memcmp(config, "af", 2) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); @@ -856,11 +828,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); } - } else if (memcmp(config, "radio", 5) == 0) { - char freq[16], bw[16]; - strcpy(freq, StrHelper::ftoa(_prefs->freq)); - strcpy(bw, StrHelper::ftoa3(_prefs->bw)); - sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); } else if (memcmp(config, "rxdelay", 7) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); } else if (memcmp(config, "txdelay", 7) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e9..9d3caf39f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -6,6 +6,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -72,7 +73,7 @@ public: uint8_t extra_sf[4]; private: - class RadioPrefs : public ConfigSerializer { + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -96,6 +97,17 @@ private: } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + // CommonRadioPrefs interface + float getFreq() const override { return _parent->freq; } + void setFreq(float f) override { _parent->freq = f; markDirty(); } + float getBandwidth() const override { return _parent->bw; } + void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } + uint8_t getSpreadFactor() const override { return _parent->sf; } + void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + uint8_t getCodingRate() const override { return _parent->cr; } + void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + float getAirtimeFactor() const override { return _parent->airtime_factor; } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } }; RadioPrefs radio; @@ -192,6 +204,9 @@ public: bridge_secret[0] = 0; owner_info[0] = 0; } + + CommonRadioPrefs* getRadioPrefs() { return &radio; } + void clearDirty() { radio.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp new file mode 100644 index 000000000..ed198c610 --- /dev/null +++ b/src/helpers/CommonRadioPrefs.cpp @@ -0,0 +1,56 @@ +#include "CommonRadioPrefs.h" +#include "TxtDataHelpers.h" +#include "Utils.h" + +bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio") == 0) { + char freq[16], bw[16]; + strcpy(freq, StrHelper::ftoa(getFreq())); + strcpy(bw, StrHelper::ftoa3(getBandwidth())); + sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate()); + return true; + } + if (memcmp(command, "set radio ", 10) == 0) { + char tmp[132]; + strcpy(tmp, &command[10]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { + setSpreadFactor(sf); + setCodingRate(cr); + setFreq(freq); + setBandwidth(bw); + // NOTE: savePrefs() should be handled by caller + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + return true; + } + if (strcmp(command, "get dutycycle") == 0) { + float dc = 100.0f / (getAirtimeFactor() + 1.0f); + int dc_int = (int)dc; + int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); + sprintf(reply, "> %d.%d%%", dc_int, dc_frac); + return true; + } + if (memcmp(command, "set dutycycle ", 14) == 0) { + float dc = atof(&command[14]); + if (dc < 1 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 1-100"); + } else { + setAirtimeFactor((100.0f / dc) - 1.0f); + // NOTE: savePrefs() should be handled by caller + float actual = 100.0f / (getAirtimeFactor() + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + } + return true; + } + return false; // not handled +} diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h new file mode 100644 index 000000000..eaec2c0aa --- /dev/null +++ b/src/helpers/CommonRadioPrefs.h @@ -0,0 +1,45 @@ +#include "ConfigSerializer.h" + +class CommonRadioPrefs : public ConfigSerializer { + bool _is_dirty = false; +protected: + CommonRadioPrefs() { } +public: + void markDirty() { _is_dirty = true; } + void clearDirty() { _is_dirty = false; } + bool isDirty() const { return _is_dirty; } + + virtual float getFreq() const = 0; + virtual void setFreq(float f) = 0; + + virtual float getBandwidth() const = 0; + virtual void setBandwidth(float bw) = 0; + + virtual uint8_t getSpreadFactor() const = 0; + virtual void setSpreadFactor(uint8_t sf) = 0; + + virtual uint8_t getCodingRate() const = 0; + virtual void setCodingRate(uint8_t cr) = 0; + + virtual float getAirtimeFactor() const = 0; + virtual void setAirtimeFactor(float af) = 0; + + // //def("cad", _parent->cad_enabled); + // //def("int_thr", _parent->interference_threshold); + // def("rxgain", _parent->rx_boosted_gain); + // #if 0 + // // NOTE: these cannot be set (yet) so don't load/save until we can. + // // also, fem_rxgain WAS mapped to wrong JSON property previously + // def("fem_rxgain", _parent->radio_fem_rxgain); + // def("fem_txgain", _parent->radio_fem_txgain); + // #endif + // def("tx", _parent->tx_power_dbm); + // def("rxdelay", _parent->rx_delay_base); + // //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded + // //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded + // //def("agc_int", _parent->agc_reset_interval); + // def("hash_mode", _parent->path_hash_mode); + // def("multi_ack", _parent->multi_acks); + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); +}; From b4f7e941fa91d9d1180ac0086fefe60def1263e9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 17:30:30 +1000 Subject: [PATCH 034/127] * CommonRadioPrefs refactors done --- examples/companion_radio/NodePrefs.h | 24 ++++- src/helpers/CommonCLI.cpp | 99 +----------------- src/helpers/CommonCLI.h | 24 ++++- src/helpers/CommonRadioPrefs.cpp | 147 ++++++++++++++++++++++++++- src/helpers/CommonRadioPrefs.h | 45 +++++--- 5 files changed, 220 insertions(+), 119 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 367880526..f1cb69ccb 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -78,11 +78,31 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } - void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return false; } + void setCadEnabled(bool en) override { /* no-op */ } + uint8_t getIntThresh() const override { return 0; } + void setIntThresh(uint8_t t) override { /* no-op */ } + uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } + void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } + uint8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + float getRxDelay() const override { return _parent->rx_delay_base; } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint8_t getAgcResetInt() const override { return 0; } + void setAgcResetInt(uint8_t secs) override { /* no-op */ } + uint8_t getHashMode() const override { return _parent->path_hash_mode; } + void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } + uint8_t getMultiAcks() const override { return _parent->multi_acks; } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return 0.5f; } // currently hard-coded + void setFloodTxDelay(float d) override { /* no-op */ } + float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded + void setDirectTxDelay(float d) override { /* no-op */ } }; RadioPrefs radio; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 4e7af30b0..2e9efa9c9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -454,27 +454,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "cad ", 4) == 0) { - _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { - _prefs->agc_reset_interval = atoi(&config[19]) / 4; - savePrefs(); - sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { + if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); @@ -527,15 +507,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); - } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - bool enabled = memcmp(&config[13], "on", 2) == 0; - _prefs->rx_boosted_gain = enabled; - savePrefs(); - if (_callbacks->setRxBoostedGain(enabled)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: unsupported"); - } } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); @@ -588,24 +559,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->node_lon = atof(&config[4]); savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0 && db <= 20.0f) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-20"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0 && f <= 2.0f) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { uint8_t m = atoi(&config[19]); if (m <= 64) { @@ -633,15 +586,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0 && f <= 2.0f) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -652,16 +596,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep *dp = 0; savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } } else if (memcmp(config, "loop.detect ", 12) == 0) { config += 12; uint8_t mode; @@ -682,11 +616,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } - } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); - savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); savePrefs(); @@ -783,17 +712,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); - } else if (memcmp(config, "int.thresh", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); - } else if (memcmp(config, "cad", 3) == 0) { - sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); - } else if (memcmp(config, "agc.reset.interval", 18) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); - } else if (memcmp(config, "allow.read.only", 15) == 0) { + if (memcmp(config, "allow.read.only", 15) == 0) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); } else if (memcmp(config, "flood.advert.interval", 21) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); @@ -814,8 +733,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.rxgain", 12) == 0) { - sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); @@ -828,18 +745,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); } - } else if (memcmp(config, "rxdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); - } else if (memcmp(config, "txdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "direct.txdelay", 14) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; @@ -850,8 +761,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sp++; } *reply = 0; // set null terminator - } else if (memcmp(config, "path.hash.mode", 14) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); @@ -862,10 +771,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } - } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { - sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); - } else if (memcmp(config, "freq", 4) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 9d3caf39f..69243f52f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -103,11 +103,31 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } - void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled; markDirty(); } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } + uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } + void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } + uint8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + float getRxDelay() const override { return _parent->rx_delay_base; } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint8_t getHashMode() const override { return _parent->path_hash_mode; } + void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } + uint8_t getMultiAcks() const override { return _parent->multi_acks; } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } }; RadioPrefs radio; diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index ed198c610..275449e6b 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -1,6 +1,7 @@ #include "CommonRadioPrefs.h" #include "TxtDataHelpers.h" #include "Utils.h" +#include "target.h" bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { if (strcmp(command, "get radio") == 0) { @@ -24,13 +25,28 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest setCodingRate(cr); setFreq(freq); setBandwidth(bw); - // NOTE: savePrefs() should be handled by caller strcpy(reply, "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } return true; } + + if (strcmp(command, "get freq") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getFreq())); + return true; + } + + if (strcmp(command, "get af") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor())); + return true; + } + if (memcmp(command, "set af ", 7) == 0) { + setAirtimeFactor(atof(&command[7])); + strcpy(reply, "OK"); + return true; + } + if (strcmp(command, "get dutycycle") == 0) { float dc = 100.0f / (getAirtimeFactor() + 1.0f); int dc_int = (int)dc; @@ -44,7 +60,6 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { setAirtimeFactor((100.0f / dc) - 1.0f); - // NOTE: savePrefs() should be handled by caller float actual = 100.0f / (getAirtimeFactor() + 1.0f); int a_int = (int)actual; int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); @@ -52,5 +67,133 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest } return true; } + + if (strcmp(command, "get int.thresh") == 0) { + sprintf(reply, "> %d", (uint32_t) getIntThresh()); + return true; + } + if (memcmp(command, "set int.thresh ", 15) == 0) { + setIntThresh(atoi(&command[15])); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get cad") == 0) { + sprintf(reply, "> %s", isCadEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set cad ", 8) == 0) { + setCadEnabled(memcmp(&command[8], "on", 2) == 0); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get radio.rxgain") == 0) { + sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off"); + return true; + } + if (memcmp(command, "set radio.rxgain ", 17) == 0) { + bool enabled = memcmp(&command[17], "on", 2) == 0; + setRxGain(enabled); + if (radio_driver.setRxBoostedGainMode(enabled)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: unsupported"); + } + return true; + } + + if (memcmp(command, "get tx", 6) == 0 && (command[6] == 0 || command[6] == ' ')) { + sprintf(reply, "> %d", (int32_t) getTxPower()); + return true; + } + if (memcmp(command, "set tx ", 7) == 0) { + setTxPower(atoi(&command[7])); + radio_driver.setTxPower(getTxPower()); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get rxdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay())); + return true; + } + if (memcmp(command, "set rxdelay ", 12) == 0) { + float db = atof(&command[12]); + if (db >= 0 && db <= 20.0f) { + setRxDelay(db); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-20"); + } + return true; + } + + if (strcmp(command, "get agc.reset.interval") == 0) { + sprintf(reply, "> %d", (uint32_t) getAgcResetInt()); + return true; + } + if (memcmp(command, "set agc.reset.interval ", 23) == 0) { + setAgcResetInt(atoi(&command[23])); + sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); + return true; + } + + if (strcmp(command, "get path.hash.mode") == 0) { + sprintf(reply, "> %d", (uint32_t)getHashMode()); + return true; + } + if (memcmp(command, "set path.hash.mode ", 19) == 0) { + const char* config = command + 19; + uint8_t mode = atoi(config); + if (mode < 3) { + setHashMode(mode); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } + return true; + } + + if (strcmp(command, "get multi.acks") == 0) { + sprintf(reply, "> %d", (uint32_t) getMultiAcks()); + return true; + } + if (memcmp(command, "set multi.acks ", 15) == 0) { + setMultiAcks(atoi(&command[15])); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get txdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay())); + return true; + } + if (memcmp(command, "set txdelay ", 12) == 0) { + float f = atof(&command[12]); + if (f >= 0 && f <= 2.0f) { + setFloodTxDelay(f); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-2"); + } + return true; + } + + if (strcmp(command, "get direct.txdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay())); + return true; + } + if (memcmp(command, "set direct.txdelay ", 19) == 0) { + float f = atof(&command[19]); + if (f >= 0 && f <= 2.0f) { + setDirectTxDelay(f); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-2"); + } + return true; + } + return false; // not handled } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index eaec2c0aa..c2aefd98b 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -24,22 +24,35 @@ public: virtual float getAirtimeFactor() const = 0; virtual void setAirtimeFactor(float af) = 0; - // //def("cad", _parent->cad_enabled); - // //def("int_thr", _parent->interference_threshold); - // def("rxgain", _parent->rx_boosted_gain); - // #if 0 - // // NOTE: these cannot be set (yet) so don't load/save until we can. - // // also, fem_rxgain WAS mapped to wrong JSON property previously - // def("fem_rxgain", _parent->radio_fem_rxgain); - // def("fem_txgain", _parent->radio_fem_txgain); - // #endif - // def("tx", _parent->tx_power_dbm); - // def("rxdelay", _parent->rx_delay_base); - // //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded - // //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded - // //def("agc_int", _parent->agc_reset_interval); - // def("hash_mode", _parent->path_hash_mode); - // def("multi_ack", _parent->multi_acks); + virtual bool isCadEnabled() const = 0; + virtual void setCadEnabled(bool en) = 0; + + virtual uint8_t getIntThresh() const = 0; + virtual void setIntThresh(uint8_t t) = 0; + + virtual uint8_t getRxGain() const = 0; + virtual void setRxGain(uint8_t g) = 0; + + virtual uint8_t getTxPower() const = 0; + virtual void setTxPower(uint8_t dbm) = 0; + + virtual float getRxDelay() const = 0; + virtual void setRxDelay(float d) = 0; + + virtual uint8_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint8_t secs) = 0; + + virtual uint8_t getHashMode() const = 0; + virtual void setHashMode(uint8_t m) = 0; + + virtual uint8_t getMultiAcks() const = 0; + virtual void setMultiAcks(uint8_t m) = 0; + + virtual float getFloodTxDelay() const = 0; + virtual void setFloodTxDelay(float d) = 0; + + virtual float getDirectTxDelay() const = 0; + virtual void setDirectTxDelay(float d) = 0; bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); }; From 21179760d37eb174b4bf824714d8f6380418a3b1 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 19:35:43 +1000 Subject: [PATCH 035/127] "Unknown command" replies --- examples/companion_radio/MyMesh.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 54be91c78..5f093f2b7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -533,10 +533,10 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection - if (from.isRemoteCLIAllowed() && handleCommand(text, sender_timestamp, reply)) { - // CLI command was handled. Let BaseChatMesh handle the sending of the reply - } else { - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + if (from.isRemoteCLIAllowed()) { + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + } } } @@ -1095,14 +1095,13 @@ void MyMesh::handleCmdFrame(size_t len) { text[tlen] = 0; // ensure null reply_buf[0] = 0; - if (handleCommand(text, 0, reply_buf)) { - out_frame[0] = RESP_CODE_CLI_REPLY; - int rlen = strlen(reply_buf); - memcpy(&out_frame[1], reply_buf, rlen); - _serial->writeFrame(out_frame, 1 + rlen); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // unsupported command + if (!handleCommand(text, 0, reply_buf)) { + strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text' } + out_frame[0] = RESP_CODE_CLI_REPLY; + int rlen = strlen(reply_buf); + memcpy(&out_frame[1], reply_buf, rlen); + _serial->writeFrame(out_frame, 1 + rlen); } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { int i = 1; uint8_t txt_type = cmd_frame[i++]; From f7c568e3d9d060940fb74cbc13a8896289b1dbd5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 20:03:49 +1000 Subject: [PATCH 036/127] onCommandDataRecv() use '>' prefix for CLI replies. --- examples/companion_radio/MyMesh.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5f093f2b7..b86af0932 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -534,8 +534,13 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3 const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection if (from.isRemoteCLIAllowed()) { - if (!handleCommand(text, sender_timestamp, reply)) { - strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + if (text[0] == '>') { // is this a CLI reply? + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, &text[1]); + } else { + *reply++ = '>'; // ensure the special 'is reply' prefix + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + } } } } From 52306bfe0f8b368d4bf47a0f7b2543d504226d7c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 20:07:28 +1000 Subject: [PATCH 037/127] legacy queueMessage() case, eg. repeater replies --- examples/companion_radio/MyMesh.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b86af0932..489fd69c0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -542,6 +542,8 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3 strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' } } + } else { + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); } } From 2c0ace2519d9c910b3b8fd1cba93f0a61e705d35 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 22:04:05 +1000 Subject: [PATCH 038/127] * new TXT_TYPE_CLI_COMMAND (3) --- examples/companion_radio/MyMesh.cpp | 18 +++++++++--------- examples/companion_radio/MyMesh.h | 2 ++ examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 4 ++-- examples/simple_secure_chat/main.cpp | 6 +++++- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/BaseChatMesh.cpp | 8 ++++++-- src/helpers/BaseChatMesh.h | 3 ++- src/helpers/TxtDataHelpers.h | 4 +++- 9 files changed, 31 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 489fd69c0..99b3196d5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -530,20 +530,20 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); } -void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, +void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) { + markConnectionActive(from); // in case this is from a server, and we have a connection + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); +} + +void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection if (from.isRemoteCLIAllowed()) { - if (text[0] == '>') { // is this a CLI reply? - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, &text[1]); - } else { - *reply++ = '>'; // ensure the special 'is reply' prefix - if (!handleCommand(text, sender_timestamp, reply)) { - strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' - } + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' } } else { - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index b60030a6d..780de35dd 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -134,6 +134,8 @@ protected: void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) override; + void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) override; void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a5..9f3c90e9b 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -704,7 +704,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks bool is_retry = (sender_timestamp == client->last_timestamp); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094fc..b786241aa 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -443,7 +443,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries bool is_retry = (sender_timestamp == client->last_timestamp); @@ -463,7 +463,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint8_t temp[166]; bool send_ack; - if (flags == TXT_TYPE_CLI_DATA) { + if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { if (client->isAdmin()) { if (is_retry) { temp[5] = 0; // no reply diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 241fe1c21..159249dfa 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -240,8 +240,12 @@ protected: } } - void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { + void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { } + + void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { + } + void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 9bfa5ec6a..b4b42f646 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -576,7 +576,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i sendAckTo(*from, ack_hash, packet->getPathHashSize()); } } - } else if (flags == TXT_TYPE_CLI_DATA) { + } else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { from->last_timestamp = sender_timestamp; from->last_activity = getRTCClock()->getCurrentTime(); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index bf8a861c9..d66aabca4 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -256,13 +256,17 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { + char *text = (char *)&data[5]; + onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know + // NOTE: no ack expected for CLI_DATA replies + } else if (flags == TXT_TYPE_CLI_COMMAND) { uint8_t temp[166]; char *command = (char *)&data[5]; char *reply = (char *)&temp[5]; *reply = 0; - onCommandDataRecv(from, packet, sender_timestamp, command, reply); // let UI know - // NOTE: no ack expected for CLI_DATA replies + onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know + // NOTE: no ack expected for CLI_COMMAND replies int text_len = strlen(reply); if (text_len > 0) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 331d1041b..a5fe54d5e 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -113,7 +113,8 @@ protected: virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len); virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; - virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; + virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0; virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index ece494f29..693a47cad 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -4,8 +4,10 @@ #include #define TXT_TYPE_PLAIN 0 // a plain text message -#define TXT_TYPE_CLI_DATA 1 // a CLI command +#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply #define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender +#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly) + #define DATA_TYPE_RESERVED 0x0000 // reserved for future use #define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps From e485d01dcbb18485ae79cc6622509f3a8f9dbdc8 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 22:49:45 +1000 Subject: [PATCH 039/127] support for CMD_SEND_TXT_MSG and new TXT_TYPE_CLI_COMMAND --- examples/companion_radio/MyMesh.cpp | 6 +++--- src/helpers/BaseChatMesh.cpp | 4 ++-- src/helpers/BaseChatMesh.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 99b3196d5..b19c8d7f5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1119,16 +1119,16 @@ void MyMesh::handleCmdFrame(size_t len) { uint8_t *pub_key_prefix = &cmd_frame[i]; i += 6; ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); - if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) { char *text = (char *)&cmd_frame[i]; int tlen = len - i; uint32_t est_timeout; text[tlen] = 0; // ensure null int result; uint32_t expected_ack; - if (txt_type == TXT_TYPE_CLI_DATA) { + if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) { msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); + result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout); expected_ack = 0; // no Ack expected } else { result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index d66aabca4..28592415c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -489,13 +489,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, return rc; } -int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { +int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); + temp[4] = (attempt & 3) | (txt_type << 2); memcpy(&temp[5], text, text_len + 1); auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index a5fe54d5e..0a8fdef4f 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -157,7 +157,7 @@ public: mesh::Packet* createSelfAdvert(const char* name); mesh::Packet* createSelfAdvert(const char* name, double lat, double lon); int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout); - int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); + int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); From 088ae3caebceb79b7df02e86475ca874eb122ec0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 23 Aug 2026 10:42:00 -0400 Subject: [PATCH 040/127] ui: opt-out for discover screen --- examples/companion_radio/MyMesh.cpp | 4 ++++ examples/companion_radio/MyMesh.h | 12 +++++++++++- examples/companion_radio/ui-new/UITask.cpp | 11 ++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01512b163..75566448f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -407,6 +407,7 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; if (max_num > disc_nodes_count) max_num = disc_nodes_count; @@ -451,6 +452,7 @@ void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len } disc_nodes_count ++; } +#endif void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; @@ -832,7 +834,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); +#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 848d21c05..25b569302 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,7 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) struct DiscoveredNode { uint8_t pubkey_prefix[9]; float snr_in; @@ -91,6 +92,7 @@ struct DiscoveredNode { char name[32]; uint8_t type; }; +#endif class MyMesh : public BaseChatMesh, public DataStoreHost { public: @@ -110,8 +112,10 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) bool requestRepeatersDiscovery(); int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); +#endif protected: float getAirtimeBudgetFactor() const override; @@ -268,12 +272,18 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - #define DISCOVERED_NODES_TABLE_SIZE 10 +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept uint32_t disc_node_req_tag = 0; uint32_t disc_nodes_count = 0; void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); +#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 355250282..0193ebbc6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -103,7 +103,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DISCOVERY, +#endif SHUTDOWN, Count // keep as last }; @@ -115,9 +117,11 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) +#endif void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -463,6 +467,7 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) } else if (_page == HomePage::DISCOVERY) { int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); display.setColor(UIColor::primary_txt); @@ -495,7 +500,7 @@ public: y = 10 + 11 * UI_RECENT_LIST_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } - +#endif } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -521,9 +526,11 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } +#endif return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -556,6 +563,7 @@ public: return true; } #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter the_mesh.requestRepeatersDiscovery(); @@ -567,6 +575,7 @@ public: discovery_disp_names = !discovery_disp_names; return true; } +#endif if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; From 47b0b7b3b8a501614d9f935eb96d794170afb5c5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 01:50:22 +1000 Subject: [PATCH 041/127] * T096, and Station G3: refactoring the 'FEM' prefs to variant-specific code * CommonCLI: 'FEM' commands removed * introduced static no-op attachDynamicPrefs() for all boards --- examples/companion_radio/MyMesh.cpp | 11 ++- examples/companion_radio/NodePrefs.h | 13 +-- examples/simple_repeater/MyMesh.cpp | 4 +- examples/simple_room_server/MyMesh.cpp | 4 +- examples/simple_sensor/SensorMesh.cpp | 4 +- src/MeshCore.h | 9 +- src/helpers/CommonCLI.cpp | 61 ++----------- src/helpers/CommonCLI.h | 7 +- src/helpers/CommonRadioPrefs.cpp | 21 +++++ src/helpers/CommonRadioPrefs.h | 13 ++- src/helpers/ConfigSerializer.h | 6 ++ src/helpers/ESP32Board.h | 3 + src/helpers/KeyValueStore.h | 11 +++ src/helpers/NRF52Board.h | 3 + src/helpers/stm32/STM32Board.h | 3 + variants/heltec_t096/T096Board.cpp | 48 +++++++++- variants/heltec_t096/T096Board.h | 12 ++- variants/station_g3_esp32/StationG3Board.cpp | 88 +++++++++++++++++-- variants/station_g3_esp32/StationG3Board.h | 17 ++-- .../waveshare_rp2040_lora/WaveshareBoard.h | 3 + variants/xiao_rp2040/XiaoRP2040Board.h | 3 + 21 files changed, 243 insertions(+), 101 deletions(-) create mode 100644 src/helpers/KeyValueStore.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b19c8d7f5..b8068dd92 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -989,8 +989,9 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); + MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -2073,6 +2074,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + // hook for variant-specific CLI processing + if (board.handleCommand(command, sender_timestamp, reply)) { + if (_prefs.isDirty()) { savePrefs(); } + return true; + } + if (memcmp(command, "set name ", 9) == 0) { if (AdvertDataParser::isValidName(&command[9])) { StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f1cb69ccb..68af09110 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -54,12 +54,8 @@ private: //def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); - #if 0 - // NOTE: these cannot be set (yet) so don't load/save until we can. - // also, fem_rxgain WAS mapped to wrong JSON property previously - def("fem_rxgain", _parent->radio_fem_rxgain); + def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously def("fem_txgain", _parent->radio_fem_txgain); - #endif def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); @@ -103,6 +99,10 @@ private: void setFloodTxDelay(float d) override { /* no-op */ } float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded void setDirectTxDelay(float d) override { /* no-op */ } + uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } + void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } + uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } + void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; @@ -177,5 +177,6 @@ public: void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } CommonRadioPrefs* getRadioPrefs() { return &radio; } - void clearDirty() { radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9f3c90e9b..d73713178 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -982,8 +982,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index b786241aa..33a67bde8 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -725,8 +725,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b4b42f646..97b39dac6 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -771,8 +771,8 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef1..434952322 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,13 +64,6 @@ public: virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported - virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemLna() const { return false; } - virtual bool isLoRaFemLnaEnabled() const { return false; } - // Software-selectable external FEM transmit gain. This is not a PA power switch. - virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemPaGain() const { return false; } - virtual bool isLoRaFemPaGainEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } @@ -79,6 +72,8 @@ public: virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2e9efa9c9..4930e81e9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -185,6 +185,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); } return; } + // hook for variant-specific CLI processing + if (_board->handleCommand(command, sender_timestamp, reply)) { + if (_prefs->isDirty()) { savePrefs(); } + return; + } if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return @@ -507,50 +512,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); - } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemLnaEnabled(true)) { - _prefs->radio_fem_rxgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemLnaEnabled(false)) { - _prefs->radio_fem_rxgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } - } else if (memcmp(config, "radio.fem.txgain ", 17) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemPaGainEnabled(true)) { - _prefs->radio_fem_txgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemPaGainEnabled(false)) { - _prefs->radio_fem_txgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); @@ -733,18 +694,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); - } - } else if (memcmp(config, "radio.fem.txgain", 16) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); - } } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 69243f52f..5735abcef 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -128,6 +128,10 @@ private: void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } + uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } + void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } + uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } + void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; @@ -226,7 +230,8 @@ public: } CommonRadioPrefs* getRadioPrefs() { return &radio; } - void clearDirty() { radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 275449e6b..60d52e157 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -3,6 +3,27 @@ #include "Utils.h" #include "target.h" +void CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { + if (strcmp(key, "fem_rxgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); + } else if (strcmp(key, "fem_txgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); + } else { + MESH_DEBUG_PRINTLN("Error: getBykey() unknown key: %s", key); + } +} +void CommonRadioPrefs::setByKey(const char* key, const char* value) { + if (strcmp(key, "fem_rxgain") == 0) { + setFEMRxGain(atoi(value)); + markDirty(); + } else if (strcmp(key, "fem_txgain") == 0) { + setFEMTxGain(atoi(value)); + markDirty(); + } else { + MESH_DEBUG_PRINTLN("Error: setBykey() unknown key: %s", key); + } +} + bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { if (strcmp(command, "get radio") == 0) { char freq[16], bw[16]; diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index c2aefd98b..de3e515fe 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -1,6 +1,8 @@ +#pragma once #include "ConfigSerializer.h" +#include "KeyValueStore.h" -class CommonRadioPrefs : public ConfigSerializer { +class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore{ bool _is_dirty = false; protected: CommonRadioPrefs() { } @@ -54,5 +56,14 @@ public: virtual float getDirectTxDelay() const = 0; virtual void setDirectTxDelay(float d) = 0; + virtual uint8_t getFEMRxGain() const = 0; + virtual void setFEMRxGain(uint8_t g) = 0; + + virtual uint8_t getFEMTxGain() const = 0; + virtual void setFEMTxGain(uint8_t g) = 0; + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); + + void setByKey(const char* key, const char* value) override; // for dynamic key/value access + void getByKey(const char* key, char* value, size_t max_len) override; }; diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 7e6d6f2a6..47ab5e81d 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -17,6 +17,7 @@ class ConfigSerializer { bool _first; int8_t _depth; + bool _dirty = false; enum OP { READ, WRITE }; @@ -62,7 +63,12 @@ protected: virtual void structure() = 0; + void markDirty() { _dirty = true; } + public: bool loadSerial(Stream& s); bool saveSerial(Stream& s); + + virtual bool isDirty() const { return _dirty; } + virtual void clearDirty() { _dirty = false; } }; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index d7eb5fee2..75428bc69 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -15,6 +15,7 @@ #include "soc/rtc.h" #include "esp_system.h" #include +#include class ESP32Board : public mesh::MainBoard { protected: @@ -51,6 +52,8 @@ public: #endif } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + // Temperature from ESP32 MCU float getMCUTemperature() override { uint32_t raw = 0; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h new file mode 100644 index 000000000..4bebb4d25 --- /dev/null +++ b/src/helpers/KeyValueStore.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +class KeyValueStore { +protected: + KeyValueStore() { } +public: + virtual void setByKey(const char* key, const char* value) { } + virtual void getByKey(const char* key, char* value, size_t max_len) { } +}; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f974..4dbfe1cab 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(NRF52_PLATFORM) @@ -57,6 +58,8 @@ public: virtual void sleep(uint32_t secs) override; bool isExternalPowered() override; + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } virtual uint32_t getResetReason() const override { return reset_reason; } diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h index 06bc768f8..016d1fb15 100644 --- a/src/helpers/stm32/STM32Board.h +++ b/src/helpers/stm32/STM32Board.h @@ -2,6 +2,7 @@ #include #include +#include class STM32Board : public mesh::MainBoard { protected: @@ -12,6 +13,8 @@ public: startup_reason = BD_STARTUP_NORMAL; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + uint8_t getStartupReason() const override { return startup_reason; } uint16_t getBattMilliVolts() override { diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 78af529d6..255a703dc 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool T096Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); -} - bool T096Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void T096Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("radio.fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("radio.fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("radio.fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 15c7e68b5..1fa7ee28a 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -4,28 +4,34 @@ #include #include #include +#include #include "LoRaFEMControl.h" class T096Board : public NRF52BoardDCDC { + KeyValueStore* _prefs = NULL; + protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif void variant_shutdown(); + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + public: RefCountedDigitalPin periph_power; LoRaFEMControl loRaFEMControl; T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {} void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; }; diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp index dd863aca6..277c2b8b7 100644 --- a/variants/station_g3_esp32/StationG3Board.cpp +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -21,10 +21,6 @@ bool StationG3Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemLna() const { - return loRaFEMControl.canControlLNA(); -} - bool StationG3Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } @@ -37,10 +33,86 @@ bool StationG3Board::setLoRaFemPaGainEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemPaGain() const { - return loRaFEMControl.canControlPAGain(); -} - bool StationG3Board::isLoRaFemPaGainEnabled() const { return loRaFEMControl.isPAGainEnabled(); } + +void StationG3Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_rxgain", gain, 7); // get initial values + setLoRaFemLnaEnabled(strcmp(gain, "1") == 0); + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool StationG3Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.canControlLNA()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.canControlLNA()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + strcpy(reply, "OK - LoRa FEM TX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index 52628eb6c..b4c5838c8 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -6,6 +6,12 @@ #include "LoRaFEMControl.h" class StationG3Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const; public: LoRaFEMControl loRaFEMControl; @@ -25,6 +31,10 @@ public: } } + void attachDynamicPrefs(KeyValueStore* prefs); + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void setPrimaryLNAEnable(bool enabled) { loRaFEMControl.setLNAEnable(enabled); } @@ -43,13 +53,6 @@ public: loRaFEMControl.setRxModeEnable(); } - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; - bool setLoRaFemPaGainEnabled(bool enable) override; - bool canControlLoRaFemPaGain() const override; - bool isLoRaFemPaGainEnabled() const override; - void powerOff() override; uint16_t getBattMilliVolts() override { diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index 694b8bd12..e7c70a5de 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -2,6 +2,7 @@ #include #include +#include // LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF // https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf @@ -32,6 +33,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index d2951c755..cad399dec 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -2,6 +2,7 @@ #include #include +#include /* * This board has no built-in way to read battery voltage. @@ -30,6 +31,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } From a1cf5bd806e27e25b3285e34c2a6b94fab0e5194 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 02:16:18 +1000 Subject: [PATCH 042/127] * refactored 'FEM' commands for HeltecTrackerV2 & HeltecV4 --- variants/heltec_t096/T096Board.cpp | 6 +-- variants/heltec_t096/T096Board.h | 3 +- .../HeltecTrackerV2Board.cpp | 48 +++++++++++++++++-- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 10 ++-- variants/heltec_v4/HeltecV4Board.cpp | 48 +++++++++++++++++-- variants/heltec_v4/HeltecV4Board.h | 10 ++-- 6 files changed, 106 insertions(+), 19 deletions(-) diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 255a703dc..ea26d2cc1 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -139,7 +139,7 @@ void T096Board::attachDynamicPrefs(KeyValueStore* prefs) { _prefs = prefs; char radio_fem_rxgain[8] = { 0 }; - _prefs->getByKey("radio.fem_rxgain", radio_fem_rxgain, 7); // get initial values + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); } @@ -158,14 +158,14 @@ bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, ch strcpy(reply, "Error: unsupported"); } else if (memcmp(&command[21], "on", 2) == 0) { if (setLoRaFemLnaEnabled(true)) { - _prefs->setByKey("radio.fem_rxgain", "1"); + _prefs->setByKey("fem_rxgain", "1"); strcpy(reply, "OK - LoRa FEM RX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else if (memcmp(&command[21], "off", 3) == 0) { if (setLoRaFemLnaEnabled(false)) { - _prefs->setByKey("radio.fem_rxgain", "0"); + _prefs->setByKey("fem_rxgain", "0"); strcpy(reply, "OK - LoRa FEM RX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 1fa7ee28a..e926c8a6a 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -26,12 +26,11 @@ public: T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {} void begin(); void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; - - bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; }; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 99b1cdfe0..753824a05 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -72,10 +72,50 @@ void HeltecTrackerV2Board::begin() { return true; } - bool HeltecTrackerV2Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a0254..5fed2b655 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -6,6 +6,10 @@ #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; public: RefCountedDigitalPin periph_power; @@ -14,13 +18,13 @@ public: HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3f13f41f6..794257d76 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -73,10 +73,50 @@ void HeltecV4Board::begin() { return true; } - bool HeltecV4Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecV4Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb37..79a690e84 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -10,6 +10,10 @@ #endif class HeltecV4Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -20,12 +24,12 @@ public: HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { if (multiplier == 0.0f) { From 7dc2d54818809a44d04d7d3132ea9a416faa331b Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 24 Aug 2026 13:45:19 +1200 Subject: [PATCH 043/127] add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tracker l1 --- examples/companion_radio/ui-new/UITask.cpp | 6 ++++++ variants/wio-tracker-l1/platformio.ini | 2 ++ 2 files changed, 8 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef17..969ac3dd4 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -102,7 +102,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -459,6 +461,7 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#ifndef UI_NO_HIBERNATE } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -470,6 +473,7 @@ public: display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } +#endif } return 5000; // next render after 5000 ms } @@ -516,10 +520,12 @@ public: return true; } #endif +#ifndef UI_NO_HIBERNATE if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; } +#endif return false; } }; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2d..19139c289 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -95,6 +96,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"' From 5a162ff4c640c6b033912ee8478590705be16018 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 17:12:56 +1000 Subject: [PATCH 044/127] * new DynamicConfigSerializer * board/variant KeyValueStore now can write to 'custom' object in Json prefs --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/NodePrefs.h | 8 ++- examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 2 +- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/CommonCLI.h | 8 ++- src/helpers/CommonRadioPrefs.cpp | 22 ++++--- src/helpers/CommonRadioPrefs.h | 6 +- src/helpers/DynamicConfigSerializer.cpp | 83 +++++++++++++++++++++++++ src/helpers/DynamicConfigSerializer.h | 20 ++++++ src/helpers/KeyValueStore.h | 4 +- 11 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 src/helpers/DynamicConfigSerializer.cpp create mode 100644 src/helpers/DynamicConfigSerializer.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b8068dd92..011df278e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -990,7 +990,7 @@ void MyMesh::begin(bool has_display) { radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 68af09110..d2a011bfd 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -2,6 +2,7 @@ #include // For uint8_t, uint32_t #include #include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -154,6 +155,8 @@ private: }; CompanionPrefs companion; + DynamicConfigSerializer custom; + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -165,9 +168,10 @@ protected: def("gps", gps); def("repeat", repeat); def("comp", companion); + def("custom", custom); } public: - NodePrefs() : radio(this), gps(this), companion(this) { + NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) { node_name[0] = 0; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); @@ -177,6 +181,8 @@ public: void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } CommonRadioPrefs* getRadioPrefs() { return &radio; } + KeyValueStore* getCustom() { return &custom; } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d73713178..ca6a3e607 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -983,7 +983,7 @@ void MyMesh::begin(FILESYSTEM *fs) { MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 33a67bde8..71b8d32a3 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -726,7 +726,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 97b39dac6..23d0cdc35 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -772,7 +772,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 5735abcef..3fb03dc5f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -7,6 +7,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -202,6 +203,8 @@ private: }; RoomPrefs room; + DynamicConfigSerializer custom; + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -218,10 +221,11 @@ protected: def("repeat", repeat); def("room", room); def("power", power); + def("custom", custom); } public: - NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this), custom(&radio) { node_name[0] = 0; password[0] = 0; guest_password[0] = 0; @@ -230,6 +234,8 @@ public: } CommonRadioPrefs* getRadioPrefs() { return &radio; } + KeyValueStore* getCustom() { return &custom; } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 60d52e157..a25df8806 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -3,25 +3,29 @@ #include "Utils.h" #include "target.h" -void CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { +bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { if (strcmp(key, "fem_rxgain") == 0) { snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); - } else if (strcmp(key, "fem_txgain") == 0) { - snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); - } else { - MESH_DEBUG_PRINTLN("Error: getBykey() unknown key: %s", key); + return true; } + if (strcmp(key, "fem_txgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); + return true; + } + return false; } -void CommonRadioPrefs::setByKey(const char* key, const char* value) { +bool CommonRadioPrefs::setByKey(const char* key, const char* value) { if (strcmp(key, "fem_rxgain") == 0) { setFEMRxGain(atoi(value)); markDirty(); - } else if (strcmp(key, "fem_txgain") == 0) { + return true; + } + if (strcmp(key, "fem_txgain") == 0) { setFEMTxGain(atoi(value)); markDirty(); - } else { - MESH_DEBUG_PRINTLN("Error: setBykey() unknown key: %s", key); + return true; } + return false; } bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index de3e515fe..96895bb2e 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -2,7 +2,7 @@ #include "ConfigSerializer.h" #include "KeyValueStore.h" -class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore{ +class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore { bool _is_dirty = false; protected: CommonRadioPrefs() { } @@ -64,6 +64,6 @@ public: bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); - void setByKey(const char* key, const char* value) override; // for dynamic key/value access - void getByKey(const char* key, char* value, size_t max_len) override; + bool setByKey(const char* key, const char* value) override; // for dynamic key/value access + bool getByKey(const char* key, char* value, size_t max_len) override; }; diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp new file mode 100644 index 000000000..7ec6dc4ef --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -0,0 +1,83 @@ +#include "DynamicConfigSerializer.h" +#include + +#define PROP_SEP_CHAR '|' +#define PROP_SEP_STR "|" +#define KEY_SEP_CHAR ':' +#define KEY_SEP_STR ":" + +bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { + if (_fallback && _fallback->setByKey(key, value)) return true; + + // TODO: guard for bad chars (':' or '|') + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + // add/replace in _config[] + char new_config[MAX_DYNAMIC_CONFG]; + new_config[0] = 0; + + int keylen = strlen(key); + for (int i = 0; i < n; i++) { + const char* item = parts[i]; + if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) { + // key exists, so omit old value from this pass (will append new value at end) + } else { + if (new_config[0]) { + strcat(new_config, PROP_SEP_STR); + } + strcat(new_config, item); + } + } + // now append new key/value (if it fits) + if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) { + strcat(new_config, key); + strcat(new_config, KEY_SEP_STR); + strcat(new_config, value); + strcpy(_config, new_config); // commit new serialized string + return true; + } + return false; // didn't fit in _config[] +} + +bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) { + if (_fallback && _fallback->getByKey(key, value, max_len)) return true; + + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + int keylen = strlen(key); + for (int i = 0; i < n; i++) { + const char* item = parts[i]; + if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) { + strncpy(value, &item[keylen+1], max_len); + value[max_len] = 0; + return true; + } + } + return false; +} + +void DynamicConfigSerializer::structure() { + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + // dynamically call def()'s + for (int i = 0; i < n; i++) { + char* item = (char *) parts[i]; + char* eq = strchr(item, KEY_SEP_CHAR); + if (eq) { + *eq = 0; // replace separator with null terminator + def(item, eq + 1, MAX_DYNAMIC_CONFG/2); // maximum HALF of total for individual property value + } + } +} diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h new file mode 100644 index 000000000..102f8e97c --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.h @@ -0,0 +1,20 @@ +#include "ConfigSerializer.h" +#include "KeyValueStore.h" + +#ifndef MAX_DYNAMIC_CONFG + #define MAX_DYNAMIC_CONFG 128 +#endif + +class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { + char _config[MAX_DYNAMIC_CONFG]; + KeyValueStore* _fallback; + +protected: + void structure() override; + +public: + DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { _config[0] = 0; } + + bool setByKey(const char* key, const char* value) override; + bool getByKey(const char* key, char* value, size_t max_len) override; +}; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h index 4bebb4d25..cc4ac455b 100644 --- a/src/helpers/KeyValueStore.h +++ b/src/helpers/KeyValueStore.h @@ -6,6 +6,6 @@ class KeyValueStore { protected: KeyValueStore() { } public: - virtual void setByKey(const char* key, const char* value) { } - virtual void getByKey(const char* key, char* value, size_t max_len) { } + virtual bool setByKey(const char* key, const char* value) { return false; } + virtual bool getByKey(const char* key, char* value, size_t max_len) { return false; } }; From 845242f4aabf8735913cbe5db7a6848c2206d240 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 17:22:15 +1000 Subject: [PATCH 045/127] * prefs, custom dirty state --- examples/companion_radio/NodePrefs.h | 4 ++-- src/helpers/CommonCLI.h | 4 ++-- src/helpers/DynamicConfigSerializer.cpp | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d2a011bfd..c79e2f0fe 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -183,6 +183,6 @@ public: CommonRadioPrefs* getRadioPrefs() { return &radio; } KeyValueStore* getCustom() { return &custom; } - bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } - void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } }; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3fb03dc5f..17b3da876 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -236,8 +236,8 @@ public: CommonRadioPrefs* getRadioPrefs() { return &radio; } KeyValueStore* getCustom() { return &custom; } - bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } - void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp index 7ec6dc4ef..dc08d7d78 100644 --- a/src/helpers/DynamicConfigSerializer.cpp +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -38,6 +38,7 @@ bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { strcat(new_config, KEY_SEP_STR); strcat(new_config, value); strcpy(_config, new_config); // commit new serialized string + markDirty(); return true; } return false; // didn't fit in _config[] From 8ccc9928a83b4035f40ccc7ad99a2fd2de03c093 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 19:42:55 +1000 Subject: [PATCH 046/127] * DynamicConfigSerializer fixes, and unit tests --- platformio.ini | 1 + src/helpers/CommonCLI.h | 4 +- src/helpers/ConfigSerializer.h | 4 +- src/helpers/DynamicConfigSerializer.cpp | 39 ++++--- src/helpers/DynamicConfigSerializer.h | 3 + .../test_config_serializer.cpp | 105 +++++++++++++----- 6 files changed, 115 insertions(+), 41 deletions(-) diff --git a/platformio.ini b/platformio.ini index ee502473c..2219c9786 100644 --- a/platformio.ini +++ b/platformio.ini @@ -171,6 +171,7 @@ build_src_filter = +<../src/Utils.cpp> +<../src/Packet.cpp> +<../src/helpers/ConfigSerializer.cpp> + +<../src/helpers/DynamicConfigSerializer.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 17b3da876..8591cdc14 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -104,13 +104,13 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } - void setCadEnabled(bool en) override { _parent->cad_enabled; markDirty(); } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } uint8_t getIntThresh() const override { return _parent->interference_threshold; } void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 47ab5e81d..e55b1b120 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -19,6 +19,7 @@ class ConfigSerializer { int8_t _depth; bool _dirty = false; +protected: enum OP { READ, WRITE }; class Context { @@ -39,13 +40,14 @@ class ConfigSerializer { const char* getToken() const { return rd_buf; } bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } + const char* getKey(uint8_t depth) { return _keys[depth]; } }; Context* _context = NULL; + int8_t getDepth() const { return _depth; } void writeComma(); -protected: ConfigSerializer() { } void def(const char* key, char* value, size_t max_len); // max_len inclusive of null diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp index dc08d7d78..b9c6c267e 100644 --- a/src/helpers/DynamicConfigSerializer.cpp +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -6,7 +6,7 @@ #define KEY_SEP_CHAR ':' #define KEY_SEP_STR ":" -bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { +bool DynamicConfigSerializer::setByKeyPrv(const char* key, const char* value) { if (_fallback && _fallback->setByKey(key, value)) return true; // TODO: guard for bad chars (':' or '|') @@ -34,16 +34,26 @@ bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { } // now append new key/value (if it fits) if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) { + if (new_config[0]) { + strcat(new_config, PROP_SEP_STR); + } strcat(new_config, key); strcat(new_config, KEY_SEP_STR); strcat(new_config, value); strcpy(_config, new_config); // commit new serialized string - markDirty(); return true; } return false; // didn't fit in _config[] } +bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { + if (setByKeyPrv(key, value)) { + markDirty(); + return true; + } + return false; +} + bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) { if (_fallback && _fallback->getByKey(key, value, max_len)) return true; @@ -66,19 +76,22 @@ bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_ } void DynamicConfigSerializer::structure() { - char tmp[MAX_DYNAMIC_CONFG]; - strcpy(tmp, _config); // make a (modifiable) copy + if (_context->op() == OP::WRITE) { + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy - const char* parts[8]; - int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); - // dynamically call def()'s - for (int i = 0; i < n; i++) { - char* item = (char *) parts[i]; - char* eq = strchr(item, KEY_SEP_CHAR); - if (eq) { - *eq = 0; // replace separator with null terminator - def(item, eq + 1, MAX_DYNAMIC_CONFG/2); // maximum HALF of total for individual property value + for (int i = 0; i < n; i++) { + char* item = (char *) parts[i]; + char* eq = strchr(item, KEY_SEP_CHAR); + if (eq) { + *eq = 0; // replace separator with null terminator + def(item, eq + 1, MAX_DYNAMIC_CONFG/2); + } } + } else { + setByKeyPrv(_context->getKey(getDepth()), _context->getToken()); } } diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h index 102f8e97c..e5dc4c7c9 100644 --- a/src/helpers/DynamicConfigSerializer.h +++ b/src/helpers/DynamicConfigSerializer.h @@ -1,3 +1,4 @@ +#pragma once #include "ConfigSerializer.h" #include "KeyValueStore.h" @@ -9,6 +10,8 @@ class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { char _config[MAX_DYNAMIC_CONFG]; KeyValueStore* _fallback; + bool setByKeyPrv(const char* key, const char* value); + protected: void structure() override; diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index 27c3c8119..dec554830 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -1,13 +1,6 @@ #include #include "helpers/ConfigSerializer.h" - -class NativeFileSystem { -public: - void mkdir(const char*) { } -}; -#define FILESYSTEM NativeFileSystem -#include "helpers/CommonCLI.h" -#undef FILESYSTEM +#include "helpers/DynamicConfigSerializer.h" #define TEST_INT_S "56" #define TEST_INT 56 @@ -192,28 +185,90 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } -TEST(NodePrefs, FemGainSettingsRoundTrip) { - NodePrefs saved; - saved.radio_fem_rxgain = 0; - saved.radio_fem_txgain = 1; +TEST(DynamicConfigSerializer, GetSet_Basic) { + DynamicConfigSerializer data; - MockPrintStream output; - ASSERT_TRUE(saved.saveSerial(output)); + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); - std::string serialised(reinterpret_cast(output.getBytes()), output.getLength()); - EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0")); - EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1")); + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("11", tmp); - MockInputStream input(serialised.c_str()); - NodePrefs loaded; - loaded.radio_fem_rxgain = 1; - loaded.radio_fem_txgain = 0; - - ASSERT_TRUE(loaded.loadSerial(input)); - EXPECT_EQ(0, loaded.radio_fem_rxgain); - EXPECT_EQ(1, loaded.radio_fem_txgain); + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); } +TEST(DynamicConfigSerializer, Set_Replaces) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool s3 = data.setByKey("age", "333"); + EXPECT_TRUE(s3); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("333", tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} + +TEST(DynamicConfigSerializer, GetUnknown_Fail) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + EXPECT_TRUE(s1); + + char tmp[32]; + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_FALSE(g2); +} + +TEST(DynamicConfigSerializer, SaveCustom_Basic) { + MockPrintStream s; + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + char tmp[128]; + memcpy(tmp, s.getBytes(), l); + tmp[l] = 0; + + const char* expect = "{age:\"11\",name:\"Scott\"}"; + EXPECT_STREQ(expect, tmp); +} + +TEST(DynamicConfigSerializer, LoadCustom_Basic) { + MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}"); + DynamicConfigSerializer data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ(TEST_INT_S, tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} // ── main ─────────────────────────────────────────────────────── From 6dad3d5ab4229fdbcbdf06bfedf4b3f228c985fd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 20:44:17 +1000 Subject: [PATCH 047/127] * companion: fix for setSpreadFactor(). "set cad ..." and "board" now implemented. --- examples/companion_radio/MyMesh.cpp | 7 ++++++- examples/companion_radio/NodePrefs.h | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 011df278e..f5a4a5a92 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -264,7 +264,7 @@ int MyMesh::getInterferenceThreshold() const { return 0; // disabled for now, until currentRSSI() problem is resolved } bool MyMesh::getCADEnabled() const { - return false; // hardware CAD before TX (disabled by default, until configurable) + return _prefs.cad_enabled; } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -2102,6 +2102,11 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "board") == 0) { + strcpy(reply, board.getManufacturerName()); + return true; + } + if (strcmp(command, "ver") == 0) { sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); return true; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c79e2f0fe..e45915b7f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -40,6 +40,7 @@ public: uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + uint8_t cad_enabled = 0; char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -52,7 +53,7 @@ private: def("bw", _parent->bw); def("sf", _parent->sf); def("cr", _parent->cr); - //def("cad", _parent->cad_enabled); + def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously @@ -75,7 +76,7 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } From 9ab13158cb62dc82222c0b7de567edabbe204cc3 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 25 Aug 2026 19:04:32 +1000 Subject: [PATCH 048/127] * fix for RPI Picow --- variants/rpi_picow/PicoWBoard.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/variants/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index 708e96558..e64590f27 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -2,6 +2,7 @@ #include #include +#include // built-ins #define PIN_VBAT_READ 26 @@ -16,6 +17,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + void onBeforeTransmit() override { digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on } From e3d0f9fcaaf69d0b57aa7489ecc052fae792c23a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 27 Aug 2026 19:05:18 +1200 Subject: [PATCH 049/127] fix cad cli command on companion --- examples/companion_radio/NodePrefs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index e45915b7f..c8a9ab830 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -81,8 +81,8 @@ private: void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } - bool isCadEnabled() const override { return false; } - void setCadEnabled(bool en) override { /* no-op */ } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } uint8_t getIntThresh() const override { return 0; } void setIntThresh(uint8_t t) override { /* no-op */ } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } From 65650bb19268388a0ba72d6d34bab939117db978 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 28 Aug 2026 13:12:59 +1000 Subject: [PATCH 050/127] New TXT_TYPE_CLI_COMMAND (3) --- docs/payloads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/payloads.md b/docs/payloads.md index a2945e27f..fb9cbaf99 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -171,8 +171,9 @@ txt_type | Value | Description | Message content | |--------|---------------------------|--------------------------------------------------------------------------| | `0x00` | plain text message | the plain text of the message | -| `0x01` | CLI command | the command text of the message | +| `0x01` | CLI data | CLI command OR reply text | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | +| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) | ## Anonymous request From e6897e8bb731275fe919ebeb94408303ff8ac8ed Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 29 Aug 2026 15:22:54 +1000 Subject: [PATCH 051/127] fix Meshnology W12 TX power --- variants/meshnology_w12/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 1168f10a0..8255e46cb 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -27,8 +27,8 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_VEXT_EN=45 -D PIN_VEXT_EN_ACTIVE=HIGH - -D LORA_TX_POWER=4 - -D MAX_LORA_TX_POWER=4 ; GC1109 datasheet says max input at TX port is +5dbm, confirmed full saturation seems to occur at around 3-4dbm + -D LORA_TX_POWER=5 ; default to 5, which gives ~22dbm + -D MAX_LORA_TX_POWER=13 ; tested with tinySA, 13 gives ~28dbm -D PIN_GPS_RX=38 -D PIN_GPS_TX=39 -D PIN_GPS_RESET=42 From 1d9a62ca4a77a4e43268d935ea0ae7d27ead9e59 Mon Sep 17 00:00:00 2001 From: Aleksei Mamlin Date: Mon, 31 Aug 2026 08:59:27 +0300 Subject: [PATCH 052/127] companion: get/set timezone offset for clock * Add tz_offset companion prefs and get/set commands for companion cli * Use tz_offset for clock on display Signed-off-by: Aleksei Mamlin --- examples/companion_radio/MyMesh.cpp | 16 ++++++++++++++++ examples/companion_radio/NodePrefs.h | 2 ++ examples/companion_radio/ui-new/UITask.cpp | 7 +------ variants/thinknode_m8/platformio.ini | 1 - 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b122b6050..ee8114ca9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2166,6 +2166,22 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "get tz.offset") == 0) { + sprintf(reply, "> %d", _prefs.tz_offset); + return true; + } + if (memcmp(command, "set tz.offset ", 14) == 0) { + int8_t tz = atof(&command[14]); + if (tz < -12 || tz > 14) { + strcpy(reply, "Error, must be from -12 to +14"); + } else { + _prefs.tz_offset = tz; + savePrefs(); + strcpy(reply, "OK"); + } + return true; + } + return false; // not handled } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c8a9ab830..f6f9b887c 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -43,6 +43,7 @@ public: uint8_t cad_enabled = 0; char default_scope_name[31]; uint8_t default_scope_key[16]; + int8_t tz_offset = 0; private: class RadioPrefs : public CommonRadioPrefs { @@ -150,6 +151,7 @@ private: def("tel_base", _parent->telemetry_mode_base); def("tel_loc", _parent->telemetry_mode_loc); def("tel_env", _parent->telemetry_mode_env); + def("tz_offset", _parent->tz_offset); } public: CompanionPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a7227bd81..2c79ab916 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -7,10 +7,6 @@ #include #endif -#ifndef UI_TZ_OFFSET - #define UI_TZ_OFFSET 0 -#endif - #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -256,8 +252,7 @@ public: #ifdef UI_SHOW_CLOCK display.setTextSize(3); uint32_t now = _rtc->getCurrentTime(); - int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... - now += (int32_t)tz * 3600; + now += (int32_t)_node_prefs->tz_offset * 3600; DateTime dt (now); sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); display.drawTextCentered(display.width() / 2, 60, tmp); diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index b8c709739..db19e4466 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -22,7 +22,6 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_ROTARY_INPUT=1 -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 - -D UI_TZ_OFFSET=-4 # GMT-4 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 From f66ff1d13fd30dceb6bd4bea29ef5b54a06841fd Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:01 -0500 Subject: [PATCH 053/127] docs: correct stale CLI default values Five "Default:" values in cli_commands.md no longer matched the firmware: - radio / freq: the default preset moved to EU/UK (Narrow) in b777a7c6, changing LORA_FREQ/BW/SF from 869.525/250/11 to 869.618/62.5/8 (platformio.ini:29-31). No variant overrides these, and LORA_CR is 5 on every path, so the full preset is 869.618,62.5,8,5. - flood.advert.interval: raised to 47 hours in 40180b8f for both repeater and room server; sensor leaves it disabled. - advert.interval: prefs store minutes/2 and the getter doubles on read, so a factory-fresh node reports 2, not 0. But savePrefs() zeroes any interval below the 60 minute minimum (CommonCLI.cpp:162-165), and it is called from every `set` handler -- so the value becomes 0 as soon as the node is configured. Documented both states, since neither alone is the whole story. - direct.txdelay: repeater defaults to 0.3, room server and sensor to 0.2. Sources: platformio.ini:29-31, simple_repeater/MyMesh.cpp:893,903-904, simple_room_server/MyMesh.cpp:650,661-662, simple_sensor/SensorMesh.cpp:716, 726-727, CommonCLI.cpp:162-165,680-681. --- docs/cli_commands.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929f..f4a43fad3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -211,7 +211,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` -**Default:** `869.525,250,11,5` +**Default:** `869.618,62.5,8,5` **Note:** Requires reboot to apply @@ -256,7 +256,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `frequency`: Frequency in MHz -**Default:** `869.525` +**Default:** `869.618` **Note:** Requires reboot to apply **Serial Only:** `set freq ` @@ -537,7 +537,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.2` +**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor) **Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. @@ -654,7 +654,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `hours`: Interval in hours (3-168) -**Default:** `12` (Repeater) - `0` (Sensor) +**Default:** `47` (Repeater, Room Server) - `0`, disabled (Sensor) --- @@ -666,7 +666,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240) -**Default:** `0` +**Default:** `2` on a factory-fresh node, then `0` (disabled) once configured. + +**Note:** A new install ships with a 2 minute zero-hop advert interval. Saving +any setting resets an interval below the 60 minute minimum to `0`, on the +assumption that the node has now been deliberately configured. To keep zero-hop +adverts running, set an explicit value in the 60-240 range. --- From f5b6ff4c14fd2bd29688bc3b64fc39e0517839ea Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:42 -0500 Subject: [PATCH 054/127] docs: fix channel index range, secret semantics and payload limit Three factual errors in companion_protocol.md: - The channel-datagram payload cap was given as 163 (MAX_FRAME_SIZE - 9 when MAX_FRAME_SIZE was still 172). It went 172 -> 176 in 62f1b11d, but simply updating the arithmetic to 167 would be worse than the stale value: 167 is only the host-frame bound (MyMesh.cpp:1265). The radio-side bound is MAX_GROUP_DATA_LENGTH = 184 - 16 - 3 = 165 (MeshCore.h:21, enforced at BaseChatMesh.cpp:544). A 166- or 167-byte payload passes the frame check, fails the radio check, and comes back as ERR_CODE_TABLE_FULL -- which this same doc describes as "retry later", so a conforming client would retry a permanently failing send forever. Documents 165 as the limit to enforce and calls out the 166-167 band explicitly. - Channel index was documented as 0-7 throughout. The bound is MAX_GROUP_CHANNELS (BaseChatMesh.cpp:927,936), which is 40 on most current variants, 8 on some and 1 on others. Clients should read max_channels from byte 3 of PACKET_DEVICE_INFO instead. Index 0 is pre-populated with the built-in Public channel but is not reserved. - The secret field was documented as "all zeros" for public channels. The firmware always hashes a real 16-byte key; the public channel ships with izOH6cXN6mrJ5e26oRXNcg== (companion_radio/MyMesh.cpp:111,1040). An all-zero secret is not a private channel and not an inert one: SHA256 over 16 zero bytes is a fixed global constant, giving a well-known channel with an all-zero AES key. searchChannelsByHash skips unnamed slots for exactly this reason (BaseChatMesh.cpp:392-401), but a named slot with a zero secret is not skipped and will absorb null-key group traffic from any node. Now documented as something not to do. --- docs/companion_protocol.md | 54 +++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 8c6b84b97..12d81bc49 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -206,7 +206,7 @@ Byte 1: 0x03 **Command Format**: ``` Byte 0: 0x1F -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) ``` **Example** (get channel 1): @@ -225,7 +225,7 @@ Byte 1: Channel Index (0-7) **Command Format**: ``` Byte 0: 0x20 -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded) Bytes 34-49: Secret (16 bytes) ``` @@ -233,8 +233,12 @@ Bytes 34-49: Secret (16 bytes) **Total Length**: 50 bytes **Channel Index**: -- Index 0: Reserved for public channels (no secret) -- Indices 1-7: Available for private channels +- Valid range is `0` to `max_channels - 1`. Read `max_channels` from byte 3 of + the `PACKET_DEVICE_INFO` response; do not assume 8. Builds ship with + `MAX_GROUP_CHANNELS` set to 1, 8 or 40 depending on the board. +- Index 0: pre-populated by the firmware with the built-in "Public" channel. + It can be overwritten like any other slot. +- An out-of-range index is rejected with `PACKET_ERROR` / `ERR_CODE_NOT_FOUND`. **Channel Name**: - UTF-8 encoded @@ -242,8 +246,18 @@ Bytes 34-49: Secret (16 bytes) - Padded with null bytes (0x00) if shorter **Secret Field** (16 bytes): -- For **private channels**: 16-byte secret -- For **public channels**: All zeros (0x00) +- Always a real 16-byte key; the firmware derives the channel hash from + `SHA256(secret)` (`BaseChatMesh.cpp:936-942`). There is no "no secret" form. +- Do **not** write an all-zero secret. `SHA256` over 16 zero bytes is a fixed, + globally identical value, so a zero-secret channel is a well-known channel + with an all-zero AES key that any node can read — not a private one. The + firmware skips *unnamed* slots when matching inbound group traffic for + exactly this reason (`BaseChatMesh.cpp:392-401`), but a slot that has been + given a name and a zero secret is not skipped, and will absorb null-key + group traffic from any node on the mesh. +- The built-in public channel uses the well-known key + `izOH6cXN6mrJ5e26oRXNcg==` (base64), i.e. + `8b3387e9c5cdea6ac9e5edbaa115cd72` in hex. **Example** (create channel "YourChannelName" at index 1 with secret): ``` @@ -265,7 +279,7 @@ Bytes 34-49: Secret (16 bytes) ``` Byte 0: 0x03 Byte 1: 0x00 -Byte 2: Channel Index (0-7) +Byte 2: Channel Index (0 .. max_channels-1) Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds) Bytes 7+: Message Text (UTF-8, variable length) ``` @@ -288,7 +302,7 @@ Bytes 7+: Message Text (UTF-8, variable length) **Command Format**: ``` Byte 0: 0x3E -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length (0xFF = flood, otherwise actual path length) Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF) Next 2 bytes (little-endian): Data Type (`data_type`, uint16) @@ -306,13 +320,23 @@ Remaining bytes: Binary payload (variable length) - Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below. **Limits**: -- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes. -- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`). +- Maximum payload length is **165** bytes — `MAX_GROUP_DATA_LENGTH = + MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3` (`src/MeshCore.h:21`). This is the + radio-side limit and the one clients should enforce. +- Two different bounds are checked, and they do not agree. The host-frame check + uses the larger `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167` + (`companion_radio/MyMesh.cpp:1265`); the radio-side check uses 165 + (`BaseChatMesh.cpp:544`). +- Payloads **above 167** are rejected with `ERR_CODE_ILLEGAL_ARG` (6). +- Payloads of **166 or 167** pass the frame check, then fail the radio-side + check and return `ERR_CODE_TABLE_FULL` (3). Despite that code's usual + meaning, such a send can never succeed — do not retry it. Keep payloads at + 165 bytes or fewer and this case cannot arise. **Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of: - `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx` -- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` -- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later +- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` (167) +- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full (retry later), **or** a payload of 166-167 bytes that exceeds the radio-side limit (permanent; see Limits above) **Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram). @@ -341,7 +365,7 @@ Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarde Byte 0: 0x1B (packet type) Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB) Bytes 2-3: Reserved (clients MUST ignore) -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct) Bytes 6-7: Data Type (uint16 little-endian) Byte 8: Data Length @@ -550,7 +574,7 @@ def parse_contact_message(data): **Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08): ``` Byte 0: 0x08 (packet type) -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length Byte 3: Text Type Bytes 4-7: Timestamp (32-bit little-endian) @@ -562,7 +586,7 @@ Bytes 8+: Message Text (UTF-8) Byte 0: 0x11 (packet type) Byte 1: SNR (signed byte, multiplied by 4) Bytes 2-3: Reserved -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length Byte 6: Text Type Bytes 7-10: Timestamp (32-bit little-endian) From 208333d51159e0b06ba32f4c6ed22b8bd9c33881 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:56 -0500 Subject: [PATCH 055/127] docs: correct pwrmgt CLI behaviour on unsupported boards The doc claimed every pwrmgt command except `get pwrmgt.support` returns "ERROR: Power management not supported" when NRF52_POWER_MANAGEMENT is not defined. `get pwrmgt.bootreason` is not inside the #ifdef (CommonCLI.cpp:787) and answers on all boards; only pwrmgt.source and pwrmgt.bootmv are gated. --- docs/nrf52_power_management.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 6d97f2c30..417ee6dd6 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -186,11 +186,19 @@ Power management status can be queried via the CLI: | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | -On boards without power management enabled, all commands except `get pwrmgt.support` return: +On boards without power management enabled, `get pwrmgt.source` and +`get pwrmgt.bootmv` return: ``` ERROR: Power management not supported ``` +`get pwrmgt.support` returns `unsupported`. `get pwrmgt.bootreason` is not +compiled out at all and answers on every board: `getResetReason()` and +`getShutdownReason()` are virtuals on the base board class, so a board that +does not override them reports `Not available` rather than an error. ESP32 +boards override the reset half with `esp_reset_reason()`, so they return a real +reset reason and `Not available` for the shutdown reason. + ## Debug Output When `MESH_DEBUG=1` is enabled, the power management module outputs: From fe5d4cc50dac989157fa2d63410fafc5786ee915 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:28:17 -0500 Subject: [PATCH 056/127] docs: remove non-existent set dutycycle from Terminal Chat CLI f6338430 added get/set dutycycle to CommonCLI and updated both CLI docs, but Terminal Chat does not use CommonCLI -- simple_secure_chat/main.cpp:479-507 has its own `set` handler supporting only af, name, lat, lon, tx and freq. Typing `set dutycycle` there returns "ERROR: unknown config". Restores `set af` as the documented command, points readers at cli_commands.md for the roles that do have dutycycle, and documents `help` (simple_secure_chat/main.cpp:510), which the client implements but the doc never listed. --- docs/terminal_chat_cli.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index 316161445..d5bac7ede 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -27,15 +27,17 @@ set lon {longitude} ``` Sets your advertisement map longitude. (decimal degrees) -``` -set dutycycle {percent} -``` -Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. - ``` set af {air-time-factor} ``` -Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead. +Sets the transmit air-time-factor. + +> **Note:** `set dutycycle` is *not* available in Terminal Chat. Every other +> firmware — Repeater, Room Server, Sensor and Companion Radio — routes its +> `set`/`get` commands through the shared radio prefs handler and does support +> it (see [CLI Commands](./cli_commands.md)). Terminal Chat is the sole +> exception: it has its own inline `set` handler covering only the six options +> listed above. ``` @@ -99,3 +101,8 @@ Resets the path to current recipient, for new path discovery. public {text} ``` Sends the text message to the built-in 'public' group channel + +``` +help +``` +Lists the available commands. From 90a4f51f915cb78c5882299b8c68cadaf8fe0b6c Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:28:51 -0500 Subject: [PATCH 057/127] docs: update stale repo URLs and build recipe The project moved to meshcore-dev/meshcore, but two links still pointed at ripplebiz/MeshCore: the GitHub Issues link in README and the git clone in the FAQ build instructions. Also fixes the FAQ build recipe: - `pio run -e RAK_4631_Repeater` does not exist; the env is spelled RAK_4631_repeater (variants/rak4631/platformio.ini:40). - LORA_FREQ was given as 867.5; the default in [arduino_base] is 869.618 (platformio.ini:29). Reworded to point at the flag rather than a value that drifts with the default preset. - Renamed the venv so it no longer collides with the cloned directory name. And one more env-name casing slip in the Raspberry Pi flashing instructions: `Heltec_V3_companion_radio_ble` should be `Heltec_v3_...`. Release artifacts are named directly from the env name (build.sh:147), and the two neighbouring examples in the same block already use the lowercase spelling. --- README.md | 2 +- docs/faq.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af56aaf33..6cc45d7f4 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,6 @@ There are a number of fairly major features in the pipeline, with no particular ## 📞 Get Support -- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. +- Report bugs and request features on the [GitHub Issues](https://github.com/meshcore-dev/meshcore/issues) page. - Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz). - Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community. diff --git a/docs/faq.md b/docs/faq.md index c7d7a118e..ade59bf00 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -537,18 +537,18 @@ Mac: python3 should be already installed. Then it should be the same for all platforms: ``` -python3 -m venv meshcore -cd meshcore && source bin/activate +python3 -m venv meshcore-venv +cd meshcore-venv && source bin/activate pip install -U platformio -git clone https://github.com/ripplebiz/MeshCore.git -cd MeshCore +git clone https://github.com/meshcore-dev/meshcore.git +cd meshcore ``` -open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5` -save, then run: +open platformio.ini and in `[arduino_base]` edit `LORA_FREQ` (it defaults to +`869.618`) to the frequency for your region, save, then run: ``` -pio run -e RAK_4631_Repeater +pio run -e RAK_4631_repeater ``` -then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater` +then you'll find `firmware.zip` in `.pio/build/RAK_4631_repeater` ### 5.10. Q: Are there other MeshCore related open source projects? @@ -592,7 +592,7 @@ For ESP-based devices (e.g. Heltec V3) you need: 1. Download the firmware file from . - Go to the website in a browser and find the section that has the firmware you need. - Click the Download button, right-click on the file you need, for example: - - `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin` + - `Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` - Non-merged bin keeps the existing Bluetooth pairing database. - `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin` - Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations. From b4e37044edf55ce96a7a4e14261ef3ec711faa68 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 3 Sep 2026 14:09:15 +1000 Subject: [PATCH 058/127] * default and reply scopes now applied --- examples/simple_sensor/SensorMesh.cpp | 76 ++++++++++++++++++++++----- examples/simple_sensor/SensorMesh.h | 6 +++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 23d0cdc35..287a3c1bf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,4 +1,5 @@ #include "SensorMesh.h" +#include /* ------------------------------ Config -------------------------------- */ @@ -262,7 +263,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { sendDirect(pkt, c->out_path, c->out_path_len); } else { unsigned long delay_millis = 0; - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } } t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); @@ -400,10 +401,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r if (sp == NULL) { strcpy(reply, "Err - bad params"); } else { + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + uint8_t pubkey[PUB_KEY_SIZE]; + *sp++ = 0; // replace space with null terminator - uint8_t pubkey[PUB_KEY_SIZE]; - int hex_len = min(sp - hex, PUB_KEY_SIZE*2); if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { uint8_t perms = atoi(sp); if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { @@ -451,6 +453,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r } } +mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) { + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + recv_pkt_region = NULL; + } else { + recv_pkt_region = ®ion_map.getWildcard(); + } + } else { + recv_pkt_region = NULL; + } + return Mesh::onRecvPacket(pkt); +} + void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; @@ -472,10 +489,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -503,7 +520,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); + if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -541,14 +558,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); if (reply) { if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -571,7 +588,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); } @@ -601,7 +618,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); if (reply) { if (from->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -708,6 +725,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise last_read_time = 0; num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; + recv_pkt_region = NULL; // defaults _prefs.airtime_factor = 1.0; @@ -820,11 +838,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params } +void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) { + if (scope.isNull()) { + sendFlood(pkt, delay_millis, path_hash_size); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, path_hash_size); + } +} + +void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + TransportKey req_scope; + bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard(); + bool req_scope_known = recv_pkt_region != NULL && !is_wildcard + && region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0; + + switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) { + case mesh::REPLY_SCOPE_REQUEST: + sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request + break; + case mesh::REPLY_SCOPE_DEFAULT: + // requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region. + // un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0 + sendFloodScoped(default_scope, packet, delay_millis, path_hash_size); + break; + case mesh::REPLY_SCOPE_NONE: + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + break; + } +} + void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -902,7 +952,7 @@ void SensorMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); unsigned long delay_millis = 0; - if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); // schedule next flood advert updateAdvertTimer(); // also schedule local advert (so they don't overlap) @@ -973,7 +1023,7 @@ void SensorMesh::loop() { } } - // is there are pending dirty contacts write needed? + // pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); dirty_contacts_expiry = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5cc..845402bae 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -81,6 +81,9 @@ public: float getTelemValue(uint8_t channel, uint8_t type); + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); + void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + protected: // current telemetry data queries float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } @@ -122,6 +125,7 @@ protected: int getInterferenceThreshold() const override; bool getCADEnabled() const override; int getAGCResetInterval() const override; + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; @@ -129,6 +133,7 @@ protected: bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; void onControlDataRecv(mesh::Packet* packet) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; + virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); private: @@ -142,6 +147,7 @@ private: CayenneLPP telemetry; TransportKeyStore key_store; RegionMap region_map; + RegionEntry* recv_pkt_region; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; From 7d501681bc3f1e93c9b404138fc143195b70cdc9 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 3 Sep 2026 17:57:09 +0800 Subject: [PATCH 059/127] fix-heltec_v4r8-lna --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 58 +++++++++++++++++++++++ variants/heltec_v4_r8/HeltecV4R8Board.h | 7 +++ variants/heltec_v4_r8/LoRaFEMControl.h | 1 + 3 files changed, 66 insertions(+) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1fb123b2f..ea2cfbedb 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -84,3 +84,61 @@ const char* HeltecV4R8Board::getManufacturerName() const { return "Heltec V4 R8 OLED"; #endif } + +bool HeltecV4R8Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool HeltecV4R8Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} + +void HeltecV4R8Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecV4R8Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 20811abb1..7ab29f962 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -11,6 +11,10 @@ #endif class HeltecV4R8Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -21,6 +25,9 @@ public: HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h index 961cfd070..4b74c2e26 100644 --- a/variants/heltec_v4_r8/LoRaFEMControl.h +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -17,6 +17,7 @@ public: void setLNAEnable(bool enabled); bool isLnaCanControl(void) { return true; } void setLnaCanControl(bool can_control) { } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } private: From 9bc6060831a0ad6064911836518e143d8a7c5ab9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 15:13:17 +1000 Subject: [PATCH 060/127] * new: REQ_TYPE_SUBSCRIBE and REQ_TYPE_UNSUBSCRIBE --- examples/simple_sensor/SensorMesh.cpp | 124 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 9 +- src/helpers/ClientACL.h | 5 ++ 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 287a3c1bf..0a78f5daf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,6 +55,8 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_SUBSCRIBE 0x10 +#define REQ_TYPE_UNSUBSCRIBE 0x11 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -74,6 +76,8 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { #endif } +/* --------------------- Cayenne LPP helpers ----------------------------*/ + static uint8_t getDataSize(uint8_t type) { switch (type) { case LPP_GPS: @@ -171,7 +175,53 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult return size; } -uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + if (ch == channel && t == type) { + return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + } + i += sz; // skip + } + return 0.0f; // not found +} + +/* ------------------ end Cayenne LPP helpers ----------------------*/ + +bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { + if (telemetry.getSize() != prev_telem_size) return true; + + auto buf = telemetry.getBuffer(); + uint8_t size = telemetry.getSize(); + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + if (abs(v - pv) > min_delta) return true; // Yes, has changed + + i += sz; // skip + } + return false; // no changes +} + +uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all @@ -235,6 +285,29 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint return ofs; } } + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint8_t reserved = payload[0]; + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + from->extra.sensor.min_deltas_len = payload[1]; + // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) + memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + + getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness + strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[6]); + } + if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + from->extra.sensor.scope_region_id = 0; + reply_data[4] = 0; // success + getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness + return 8; + } return 0; // unknown command } @@ -548,7 +621,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(×tamp, data, 4); if (timestamp > from->last_timestamp) { // prevent replay attacks - uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5); + uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from->last_timestamp = timestamp; @@ -726,6 +799,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; + prev_telem_size = 0; // defaults _prefs.airtime_factor = 1.0; @@ -916,23 +990,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - auto buf = telemetry.getBuffer(); - uint8_t size = telemetry.getSize(); - uint8_t i = 0; - - while (i + 2 < size) { - // Get channel # - uint8_t ch = buf[i++]; - // Get data type - uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); - - if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - } - i += sz; // skip - } - return 0.0f; // not found + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { @@ -982,6 +1040,34 @@ void SensorMesh::loop() { // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // compare with previous telemetry, check if any deltas are greater than subscriber minimums + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); + if (r == NULL) continue; // unknown region scope + if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + TransportKey scope; + if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { + uint8_t tlen = telemetry.getSize(); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client + memcpy(reply_data, ×tamp, 4); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + if (reply) { + if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + sendDirect(reply, c->out_path, c->out_path_len, 0); + } else { + sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1); + } + } + } + } + } + memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle + prev_telem_size = telemetry.getSize(); + onSensorDataRead(); last_read_time = curr; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 845402bae..7ea87f206 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -96,12 +96,12 @@ protected: bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts - enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT }; struct Trigger { uint32_t timestamp; - AlertPriority pri; uint32_t expected_acks[4]; + AlertPriority pri; int8_t curr_contact_idx; uint8_t attempt; unsigned long send_expiry; @@ -145,6 +145,8 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; + uint8_t prev_telem_size; + uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map; RegionEntry* recv_pkt_region; @@ -159,8 +161,9 @@ private: uint8_t pending_sf; uint8_t pending_cr; + bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); - uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); void sendAlert(const ClientInfo* c, Trigger* t); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index e06544647..9fde35ecb 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -28,6 +28,11 @@ struct ClientInfo { unsigned long ack_timeout; uint8_t push_failures; } room; + struct { + uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber + uint8_t min_deltas_len; + uint8_t min_deltas[14]; // LPP encoded + } sensor; } extra; bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } From 3a5a6b90ab3d9e65950dd4e8e03026ce73da4103 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 22:02:49 +1000 Subject: [PATCH 061/127] * sensor: CLI "sub" and "unsub" commands --- examples/simple_sensor/SensorMesh.cpp | 68 ++++++++++++++++++++++++++- examples/simple_sensor/SensorMesh.h | 2 +- examples/simple_sensor/main.cpp | 2 +- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 0a78f5daf..008cbcfc0 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -193,6 +193,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } +static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { + const char* parts[3]; + int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); + uint8_t len = 0; + for (int i = 0; i < n && len + 6 <= max_len; i++) { + const char* cp = strchr(parts[i], ':'); + if (cp) { + uint8_t t; + float factor = 1.0f; + cp++; // skip the ':' + char* ep = strchr(cp, 0) - 1; // find LAST char + if (*ep == 'V') { // Volts + t = LPP_VOLTAGE; + } else if (*ep == 'W') { // Watts + t = LPP_POWER; + } else if (*ep == 'C') { // Celcius + t = LPP_TEMPERATURE; + } else if (*ep == 'P') { // Pascals + t = LPP_BAROMETRIC_PRESSURE; + } else if (*ep == 'A') { // Amps + t = LPP_CURRENT; + } else if (*ep == 'm') { + t = LPP_DISTANCE; factor = 0.001f; + } else { + t = 0; + } + + if (t) { + dest[len++] = atoi(parts[i]); // channel number + dest[len++] = t; // LPP type + len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); + } + } + } + return len; +} + /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -453,7 +490,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* return 13; // reply length } -void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { +void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -502,6 +539,33 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r Serial.printf("\n"); } reply[0] = 0; + } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + if (command[3] == ' ') { + // compile params as LPP data, eg. "sub 1:0.2V" + from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } else { + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + if (from->extra.sensor.scope_region_id) { + strcpy(reply, "OK - subscribed"); + } else { + strcpy(reply, "Err - region scope needed"); + } + } else { + strcpy(reply, "Err - no permission"); + } + } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe + from->extra.sensor.scope_region_id = 0; + strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -676,7 +740,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i uint8_t temp[166]; char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; - handleCommand(sender_timestamp, command, reply); + handleCommand(from, sender_timestamp, command, reply); int text_len = strlen(reply); if (text_len > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 7ea87f206..6d5857d05 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -51,7 +51,7 @@ public: SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); void loop(); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply); // CommonCLI callbacks const char* getFirmwareVer() override { return FIRMWARE_VERSION; } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a7..9e3b230b6 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -137,7 +137,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } From 95214e8c40264eb374ed2a94d6905619ae557650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Thu, 16 Jul 2026 19:59:12 +0000 Subject: [PATCH 062/127] Ensure command buffers stay NUL-terminated to prevent overflow The serial command buffers must stay NUL-terminated within their bounds: if they ever aren't, strlen() can return >= sizeof(command) and the read loop would index past the buffer. Additionally, a full buffer now becomes a completed line (end-of-line marker placed inside the buffer, NUL terminator kept) instead of overwriting the terminator and silently corrupting the buffer for the next pass. Applies to the serial CLI readers of the repeater, room server, sensor and secure chat examples, and to the CLI rescue reader of the companion example. --- examples/companion_radio/MyMesh.cpp | 12 ++++++++++-- examples/simple_repeater/main.cpp | 12 ++++++++++-- examples/simple_room_server/main.cpp | 12 ++++++++++-- examples/simple_secure_chat/main.cpp | 12 ++++++++++-- examples/simple_sensor/main.cpp | 12 ++++++++++-- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ee8114ca9..b4e515147 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2187,6 +2187,13 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); + // `cli_command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(cli_command) and the loop below would + // then index past the buffer, so clamp defensively. + if (len >= (int)sizeof(cli_command)) { + cli_command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(cli_command)-1) { char c = Serial.read(); if (c != '\n') { @@ -2195,8 +2202,9 @@ void MyMesh::checkCLIRescueCmd() { } Serial.print(c); // echo } - if (len == sizeof(cli_command)-1) { // command buffer full - cli_command[sizeof(cli_command)-1] = '\r'; + if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line + cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer + cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && cli_command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68e..1f71da74d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -125,6 +125,13 @@ void setup() { void loop() { // Handle Serial CLI 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 (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -134,8 +141,9 @@ void loop() { } if (c == '\r') break; } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + 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 diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff39..227ee2cbd 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -105,6 +105,13 @@ void setup() { void loop() { 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 (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -113,8 +120,9 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + 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 diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 159249dfa..2e6ba7126 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -530,6 +530,13 @@ public: BaseChatMesh::loop(); 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 (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -538,8 +545,9 @@ public: } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + 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 diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a7..749ff6ef1 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -122,6 +122,13 @@ void setup() { void loop() { 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 (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -130,8 +137,9 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + 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 From 1d5283de767881e5f9c5ad2b4f564d422779df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Wed, 15 Jul 2026 08:18:59 +0000 Subject: [PATCH 063/127] Improve null checks and index validation in StaticPoolPacketManager --- src/helpers/StaticPoolPacketManager.cpp | 9 +++++++-- src/helpers/StaticPoolPacketManager.h | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0c..8a8eefe8c 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -44,7 +44,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) { } mesh::Packet* PacketQueue::removeByIdx(int i) { - if (i >= _num) return NULL; // invalid index + if (i < 0 || i >= _num) return NULL; // invalid index mesh::Packet* item = _table[i]; _num--; @@ -80,10 +80,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() { } void StaticPoolPacketManager::free(mesh::Packet* packet) { - unused.add(packet, 0, 0); + if (packet == NULL) return; + if (!unused.add(packet, 0, 0)) { + MESH_DEBUG_PRINTLN("free: unused queue full, possible double-free detected"); + } } void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { + if (packet == NULL) return; if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); free(packet); @@ -115,6 +119,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { } void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { + if (packet == NULL) return; if (!rx_queue.add(packet, 0, scheduled_for)) { MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet"); free(packet); diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e0..f63700e51 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -14,7 +14,10 @@ public: bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); int count() const { return _num; } int countBefore(uint32_t now) const; - mesh::Packet* itemAt(int i) const { return _table[i]; } + mesh::Packet* itemAt(int i) const { + if (i < 0 || i >= _num) return NULL; + return _table[i]; + } mesh::Packet* removeByIdx(int i); }; From 8a418faf93c5a0b94c1b60c8a7da8b811a43b002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Fri, 4 Sep 2026 21:04:20 +0100 Subject: [PATCH 064/127] Add isGPSDetected() capability probe to SensorManager Lets applications know whether a GPS was actually detected (EnvironmentSensorManager reports its gps_detected state), e.g. to hide GPS-dependent UI when no receiver is attached. --- src/helpers/SensorManager.h | 1 + src/helpers/sensors/EnvironmentSensorManager.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c22..85b9a1ee0 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -23,6 +23,7 @@ public: virtual const char* getSettingValue(int i) const { return NULL; } virtual bool setSettingValue(const char* name, const char* value) { return false; } virtual LocationProvider* getLocationProvider() { return NULL; } + virtual bool isGPSDetected() const { return false; } // Helper functions to manage setting by keys (useful in many places ...) const char* getSettingByKey(const char* key) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c896..17706cbc4 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -38,6 +38,7 @@ public: #if ENV_INCLUDE_GPS EnvironmentSensorManager(LocationProvider &location): _location(&location){}; LocationProvider* getLocationProvider() { return _location; } + bool isGPSDetected() const override { return gps_detected; } #else EnvironmentSensorManager(){}; #endif From 8fd689311772783b3fac106f002b51dd7b884e24 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 4 Sep 2026 16:03:41 -0700 Subject: [PATCH 065/127] feat: add T-Beam 1W thermal fan control --- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 341 +++++++++++++++++++++- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 39 ++- variants/lilygo_tbeam_1w/variant.h | 19 +- 3 files changed, 387 insertions(+), 12 deletions(-) diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index 1719d7333..e82e4623b 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -1,5 +1,12 @@ #include "TBeam1WBoard.h" +#include +#include +#include +#include + +static const int FAN_PWM_MAX = (1 << FAN_PWM_RES_BITS) - 1; + void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -15,31 +22,57 @@ void TBeam1WBoard::begin() { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); - // Initialize fan control (on by default - 1W PA can overheat) + // NTC ADC (PA-adjacent thermistor) + pinMode(NTC_PIN, INPUT); + analogSetPinAttenuation(NTC_PIN, ADC_11db); + analogReadResolution(12); + + // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); digitalWrite(FAN_CTRL_PIN, HIGH); + _temp_c = readNtcTempC(); + applyDuty(100); + startFanTask(); +} + +void TBeam1WBoard::startFanTask() { + if (_fan_task) return; + xTaskCreate(fanTaskThunk, "tbeam1w_fan", 4096, this, 1, &_fan_task); +} + +void TBeam1WBoard::fanTaskThunk(void* arg) { + auto* self = static_cast(arg); + for (;;) { + if (!self->_stopped) { + self->updateFan(); + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } } void TBeam1WBoard::onBeforeTransmit() { - // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() digitalWrite(LED_PIN, HIGH); // TX LED on + _tx_active = true; + if (_mode == FAN_AUTO && _manual_duty < 0 && !_stopped) { + applyDuty(FAN_TX_FLOOR_PCT); + } } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + _tx_active = false; } uint16_t TBeam1WBoard::getBattMilliVolts() { // T-Beam 1W uses 7.4V battery with voltage divider - // ADC reads through divider - adjust multiplier based on actual divider ratio analogReadResolution(12); uint32_t raw = 0; for (int i = 0; i < 8; i++) { raw += analogRead(BATTERY_PIN); } raw = raw / 8; - // Assuming voltage divider ratio from ADC_MULTIPLIER - // 3.3V reference, 12-bit ADC (4095 max) return static_cast((raw * 3300 * ADC_MULTIPLIER) / 4095); } @@ -48,6 +81,9 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + _stopped = true; + applyDuty(0); + // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -55,17 +91,304 @@ void TBeam1WBoard::powerOff() { digitalWrite(SX126X_POWER_EN, LOW); radio_powered = false; - // Turn off LED and fan digitalWrite(LED_PIN, LOW); - digitalWrite(FAN_CTRL_PIN, LOW); ESP32Board::powerOff(); } void TBeam1WBoard::setFanEnabled(bool enabled) { - digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); + applyDuty(enabled ? 100 : 0); } bool TBeam1WBoard::isFanEnabled() const { - return digitalRead(FAN_CTRL_PIN) == HIGH; + return _duty_pct > 0; +} + +float TBeam1WBoard::readNtcTempC() { + analogReadMilliVolts(NTC_PIN); // settle + uint32_t sum = 0; + for (int i = 0; i < 8; i++) { + uint32_t sample_mv = analogReadMilliVolts(NTC_PIN); + // GPIO14 is ADC2 on ESP32-S3. Wi-Fi/ESP-NOW arbitration failures are + // reported by Arduino as 0 mV; never average a failed sample into a + // plausible-but-low temperature. + if (sample_mv == 0 || sample_mv >= NTC_VCC_MV) return NAN; + sum += sample_mv; + } + float mv = sum / 8.0f; + + // R_ntc = R_fixed * (Vcc - V) / V for 3.3V-NTC-ADC-10k-GND + float r_ntc = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv; + if (r_ntc <= 0.0f) return NAN; + + float temp_k = 1.0f / (1.0f / 298.15f + (1.0f / NTC_B) * logf(r_ntc / NTC_R25)); + return temp_k - 273.15f; +} + +bool TBeam1WBoard::ntcImplausible(float temp_c) const { + return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; +} + +int TBeam1WBoard::rampDuty(float temp_c) const { + if (temp_c < (float)_lo_c) return 0; + if (temp_c >= (float)_hi_c) return 100; + float span = (float)(_hi_c - _lo_c); + if (span <= 0.0f) return 100; + float t = (temp_c - (float)_lo_c) / span; + return FAN_MIN_DUTY_PCT + (int)((100 - FAN_MIN_DUTY_PCT) * t + 0.5f); +} + +bool TBeam1WBoard::isTxCooling(uint32_t now) { + if (_tx_active) return true; + if (!_tx_cooldown_active) return false; + if ((int32_t)(now - _tx_until_ms) >= 0) { + _tx_cooldown_active = false; + return false; + } + return true; +} + +int TBeam1WBoard::cooldownSecs() { + if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; + if (!_tx_cooldown_active) return 0; + int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); + if (remain_ms <= 0) { + _tx_cooldown_active = false; + return 0; + } + return (remain_ms + 999) / 1000; +} + +void TBeam1WBoard::applyDuty(int pct) { + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + _duty_pct = pct; + + if (_drive == FAN_DRIVE_PWM) { + if (!_pwm_attached) { + ledcSetup(FAN_PWM_CHANNEL, FAN_PWM_FREQ_HZ, FAN_PWM_RES_BITS); + ledcAttachPin(FAN_CTRL_PIN, FAN_PWM_CHANNEL); + _pwm_attached = true; + } + uint32_t ticks = ((uint32_t)pct * FAN_PWM_MAX + 50) / 100; + ledcWrite(FAN_PWM_CHANNEL, ticks); + } else { + if (_pwm_attached) { + ledcDetachPin(FAN_CTRL_PIN); + _pwm_attached = false; + pinMode(FAN_CTRL_PIN, OUTPUT); + } + digitalWrite(FAN_CTRL_PIN, pct > 0 ? HIGH : LOW); + } +} + +void TBeam1WBoard::updateFan() { + float t = readNtcTempC(); + _temp_c = t; + bool tx_cooling = isTxCooling(millis()); + + int duty; + if (_manual_duty >= 0) { + duty = _manual_duty; + } else if (_mode == FAN_ON) { + duty = 100; + } else if (_mode == FAN_OFF) { + duty = 0; + } else if (ntcImplausible(t)) { + duty = 100; // fail-safe: treat bad NTC as hot + } else if (_drive == FAN_DRIVE_PWM) { + duty = rampDuty(t); + } else { + // Thermal hysteresis only: on at hi, off below lo. TX cooldown is applied + // after this and must not latch _thermal_on, or a TX at 30C keeps the fan + // running until temp dips under lo. + if (t >= (float)_hi_c) _thermal_on = true; + else if (t < (float)_lo_c) _thermal_on = false; + duty = _thermal_on ? 100 : 0; + } + + if (_mode == FAN_AUTO && _manual_duty < 0 && !ntcImplausible(t)) { + if (tx_cooling && duty < FAN_TX_FLOOR_PCT) duty = FAN_TX_FLOOR_PCT; + } + + applyDuty(duty); +} + +bool TBeam1WBoard::persistKey(const char* key, const char* value) { + return _prefs && _prefs->setByKey(key, value); +} + +bool TBeam1WBoard::parseIntArg(const char* text, int& value) { + if (!text || !*text) return false; + errno = 0; + char* end = nullptr; + long parsed = strtol(text, &end, 10); + if (errno == ERANGE || end == text || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + value = (int)parsed; + return true; +} + +void TBeam1WBoard::loadFanPrefs() { + if (!_prefs) return; + + char buf[12]; + buf[0] = 0; + if (_prefs->getByKey("fan", buf, 11)) { + if (strcmp(buf, "auto") == 0) _mode = FAN_AUTO; + else if (strcmp(buf, "off") == 0) _mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) _mode = FAN_ON; + } + + buf[0] = 0; + if (_prefs->getByKey("fan_drv", buf, 11)) { + if (strcmp(buf, "onoff") == 0) _drive = FAN_DRIVE_ONOFF; + else if (strcmp(buf, "pwm") == 0) _drive = FAN_DRIVE_PWM; + } + + int lo = _lo_c; + int hi = _hi_c; + buf[0] = 0; + if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); + buf[0] = 0; + if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + if (lo >= 0 && hi <= 120 && lo < hi) { + _lo_c = lo; + _hi_c = hi; + } + + _manual_duty = -1; +} + +void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + loadFanPrefs(); + updateFan(); +} + +const char* TBeam1WBoard::modeName() const { + if (_manual_duty >= 0) return "manual"; + if (_mode == FAN_AUTO) return "auto"; + if (_mode == FAN_OFF) return "off"; + return "on"; +} + +const char* TBeam1WBoard::driveName() const { + return _drive == FAN_DRIVE_ONOFF ? "onoff" : "pwm"; +} + +bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + (void)sender_timestamp; + + if (strcmp(command, "get fan") == 0) { + int cd = cooldownSecs(); + if (ntcImplausible(_temp_c)) { + sprintf(reply, "> %s n/a duty=%d%% %s cd=%ds", + modeName(), (int)_duty_pct, driveName(), cd); + } else { + sprintf(reply, "> %s %.1fC duty=%d%% %s cd=%ds", + modeName(), (double)_temp_c, (int)_duty_pct, driveName(), cd); + } + return true; + } + + if (strncmp(command, "set fan.lo ", 11) == 0) { + int lo; + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= _hi_c || lo > 100) { + strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); + } else if (!persistKey("fan_lo", &command[11])) { + strcpy(reply, "Error: failed to save fan.lo"); + } else { + _lo_c = lo; + sprintf(reply, "OK - fan.lo %d", lo); + } + return true; + } + + if (strncmp(command, "set fan.hi ", 11) == 0) { + int hi; + if (!parseIntArg(&command[11], hi) || hi <= _lo_c || hi > 120) { + strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); + } else if (!persistKey("fan_hi", &command[11])) { + strcpy(reply, "Error: failed to save fan.hi"); + } else { + _hi_c = hi; + sprintf(reply, "OK - fan.hi %d", hi); + } + return true; + } + + if (strncmp(command, "set fan.drive ", 14) == 0) { + const char* arg = &command[14]; + if (strcmp(arg, "pwm") == 0) { + if (!persistKey("fan_drv", "pwm")) { + strcpy(reply, "Error: failed to save fan.drive"); + } else { + _drive = FAN_DRIVE_PWM; + applyDuty(_duty_pct); + strcpy(reply, "OK - fan.drive pwm"); + } + } else if (strcmp(arg, "onoff") == 0) { + if (!persistKey("fan_drv", "onoff")) { + strcpy(reply, "Error: failed to save fan.drive"); + } else { + _drive = FAN_DRIVE_ONOFF; + applyDuty(_duty_pct); + strcpy(reply, "OK - fan.drive onoff"); + } + } else { + strcpy(reply, "Error: fan.drive must be pwm or onoff"); + } + return true; + } + + if (strncmp(command, "set fan.duty ", 13) == 0) { + int duty; + if (!parseIntArg(&command[13], duty) || duty < 0 || duty > 100) { + strcpy(reply, "Error: fan.duty must be 0-100"); + } else { + _manual_duty = duty; + applyDuty(duty); + sprintf(reply, "OK - fan.duty %d (not saved)", duty); + } + return true; + } + + if (strncmp(command, "set fan ", 8) == 0) { + const char* arg = &command[8]; + if (strcmp(arg, "on") == 0) { + if (!persistKey("fan", "on")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_ON; + _manual_duty = -1; + applyDuty(100); + strcpy(reply, "OK - fan on"); + } + } else if (strcmp(arg, "off") == 0) { + if (!persistKey("fan", "off")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_OFF; + _manual_duty = -1; + applyDuty(0); + strcpy(reply, "OK - fan off"); + } + } else if (strcmp(arg, "auto") == 0) { + if (!persistKey("fan", "auto")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_AUTO; + _manual_duty = -1; + updateFan(); + strcpy(reply, "OK - fan auto"); + } + } else { + strcpy(reply, "Error: fan must be on, off, or auto"); + } + return true; + } + + return false; } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index d999dfd4c..e96923c53 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "variant.h" @@ -28,18 +30,53 @@ // - Battery must support 2A+ discharge for high-power TX class TBeam1WBoard : public ESP32Board { +public: + enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; + enum FanDrive { FAN_DRIVE_PWM, FAN_DRIVE_ONOFF }; + private: bool radio_powered = false; + bool _stopped = false; + bool _pwm_attached = false; + KeyValueStore* _prefs = nullptr; + FanMode _mode = FAN_AUTO; + FanDrive _drive = FAN_DRIVE_ONOFF; + int _lo_c = FAN_DEFAULT_LO_C; + int _hi_c = FAN_DEFAULT_HI_C; + int _manual_duty = -1; // -1 = follow mode; 0..100 = CLI override + bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this + volatile float _temp_c = NAN; + volatile int _duty_pct = 100; + volatile bool _tx_active = false; + volatile bool _tx_cooldown_active = false; + volatile uint32_t _tx_until_ms = 0; + TaskHandle_t _fan_task = nullptr; + + void startFanTask(); + void updateFan(); + void applyDuty(int pct); + float readNtcTempC(); + int rampDuty(float temp_c) const; + int cooldownSecs(); + bool isTxCooling(uint32_t now); + bool ntcImplausible(float temp_c) const; + bool persistKey(const char* key, const char* value); + static bool parseIntArg(const char* text, int& value); + void loadFanPrefs(); + const char* modeName() const; + const char* driveName() const; + static void fanTaskThunk(void* arg); public: void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; - // Fan control methods void setFanEnabled(bool enabled); bool isFanEnabled() const; }; diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index f6807e56b..a67e2e846 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -77,11 +77,26 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 3.0 -// NTC temperature sensor +// NTC thermistor (Murata NCP18XH103F03RB, 10k, B25/50=3380K) +// Divider: 3.3V -> NTC -> GPIO14 -> 10k pull-down -> GND (Vadc rises with temp) #define NTC_PIN 14 +#define NTC_B 3380.0f +#define NTC_R25 10000.0f +#define NTC_R_FIXED 10000.0f +#define NTC_VCC_MV 3300.0f -// Fan control +// Fan control (GPIO41). Default auto + on/off: NTC is PA-adjacent PCB temp, +// not die temp, so trip well below the SX1262/ESP32 85C operating limit. +// This fan/MOSFET path does not respond to PWM below 100% duty. #define FAN_CTRL_PIN 41 +#define FAN_PWM_CHANNEL 4 +#define FAN_PWM_FREQ_HZ 25000 +#define FAN_PWM_RES_BITS 8 +#define FAN_MIN_DUTY_PCT 40 +#define FAN_TX_COOLDOWN_MS 15000 +#define FAN_TX_FLOOR_PCT 100 +#define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) +#define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U From 6c1f4c09a6e46603ee291e36bb8d8f6446e75b33 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 4 Sep 2026 16:51:53 -0700 Subject: [PATCH 066/127] Fix T-Beam 1W fan races and remove PWM controls --- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 222 +++++++++------------- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 25 +-- variants/lilygo_tbeam_1w/variant.h | 11 +- 3 files changed, 102 insertions(+), 156 deletions(-) diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index e82e4623b..e04199554 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -5,8 +5,6 @@ #include #include -static const int FAN_PWM_MAX = (1 << FAN_PWM_RES_BITS) - 1; - void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -30,8 +28,8 @@ void TBeam1WBoard::begin() { // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); digitalWrite(FAN_CTRL_PIN, HIGH); + _fan_on = true; _temp_c = readNtcTempC(); - applyDuty(100); startFanTask(); } @@ -43,26 +41,30 @@ void TBeam1WBoard::startFanTask() { void TBeam1WBoard::fanTaskThunk(void* arg) { auto* self = static_cast(arg); for (;;) { - if (!self->_stopped) { - self->updateFan(); - } + self->updateFan(); vTaskDelay(pdMS_TO_TICKS(1000)); } } void TBeam1WBoard::onBeforeTransmit() { digitalWrite(LED_PIN, HIGH); // TX LED on + portENTER_CRITICAL(&_fan_mux); _tx_active = true; - if (_mode == FAN_AUTO && _manual_duty < 0 && !_stopped) { - applyDuty(FAN_TX_FLOOR_PCT); + if (_mode == FAN_AUTO && !_stopped) { + setFanOutputLocked(true); } + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off - _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; - _tx_cooldown_active = true; + portENTER_CRITICAL(&_fan_mux); + if (!_stopped) { + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + } _tx_active = false; + portEXIT_CRITICAL(&_fan_mux); } uint16_t TBeam1WBoard::getBattMilliVolts() { @@ -81,8 +83,12 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + portENTER_CRITICAL(&_fan_mux); _stopped = true; - applyDuty(0); + _tx_active = false; + _tx_cooldown_active = false; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -97,11 +103,16 @@ void TBeam1WBoard::powerOff() { } void TBeam1WBoard::setFanEnabled(bool enabled) { - applyDuty(enabled ? 100 : 0); + portENTER_CRITICAL(&_fan_mux); + setFanOutputLocked(enabled && !_stopped); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::isFanEnabled() const { - return _duty_pct > 0; + portENTER_CRITICAL(&_fan_mux); + bool enabled = _fan_on; + portEXIT_CRITICAL(&_fan_mux); + return enabled; } float TBeam1WBoard::readNtcTempC() { @@ -129,16 +140,7 @@ bool TBeam1WBoard::ntcImplausible(float temp_c) const { return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; } -int TBeam1WBoard::rampDuty(float temp_c) const { - if (temp_c < (float)_lo_c) return 0; - if (temp_c >= (float)_hi_c) return 100; - float span = (float)(_hi_c - _lo_c); - if (span <= 0.0f) return 100; - float t = (temp_c - (float)_lo_c) / span; - return FAN_MIN_DUTY_PCT + (int)((100 - FAN_MIN_DUTY_PCT) * t + 0.5f); -} - -bool TBeam1WBoard::isTxCooling(uint32_t now) { +bool TBeam1WBoard::isTxCoolingLocked(uint32_t now) { if (_tx_active) return true; if (!_tx_cooldown_active) return false; if ((int32_t)(now - _tx_until_ms) >= 0) { @@ -148,7 +150,7 @@ bool TBeam1WBoard::isTxCooling(uint32_t now) { return true; } -int TBeam1WBoard::cooldownSecs() { +int TBeam1WBoard::cooldownSecsLocked() { if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; if (!_tx_cooldown_active) return 0; int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); @@ -159,59 +161,41 @@ int TBeam1WBoard::cooldownSecs() { return (remain_ms + 999) / 1000; } -void TBeam1WBoard::applyDuty(int pct) { - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - _duty_pct = pct; - - if (_drive == FAN_DRIVE_PWM) { - if (!_pwm_attached) { - ledcSetup(FAN_PWM_CHANNEL, FAN_PWM_FREQ_HZ, FAN_PWM_RES_BITS); - ledcAttachPin(FAN_CTRL_PIN, FAN_PWM_CHANNEL); - _pwm_attached = true; - } - uint32_t ticks = ((uint32_t)pct * FAN_PWM_MAX + 50) / 100; - ledcWrite(FAN_PWM_CHANNEL, ticks); - } else { - if (_pwm_attached) { - ledcDetachPin(FAN_CTRL_PIN); - _pwm_attached = false; - pinMode(FAN_CTRL_PIN, OUTPUT); - } - digitalWrite(FAN_CTRL_PIN, pct > 0 ? HIGH : LOW); - } +void TBeam1WBoard::setFanOutputLocked(bool enabled) { + _fan_on = enabled; + digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); } void TBeam1WBoard::updateFan() { float t = readNtcTempC(); - _temp_c = t; - bool tx_cooling = isTxCooling(millis()); + uint32_t now = millis(); - int duty; - if (_manual_duty >= 0) { - duty = _manual_duty; - } else if (_mode == FAN_ON) { - duty = 100; + portENTER_CRITICAL(&_fan_mux); + if (_stopped) { + portEXIT_CRITICAL(&_fan_mux); + return; + } + + _temp_c = t; + bool tx_cooling = isTxCoolingLocked(now); + + bool enabled; + if (_mode == FAN_ON) { + enabled = true; } else if (_mode == FAN_OFF) { - duty = 0; + enabled = false; } else if (ntcImplausible(t)) { - duty = 100; // fail-safe: treat bad NTC as hot - } else if (_drive == FAN_DRIVE_PWM) { - duty = rampDuty(t); + enabled = true; // fail-safe: treat bad NTC as hot } else { - // Thermal hysteresis only: on at hi, off below lo. TX cooldown is applied - // after this and must not latch _thermal_on, or a TX at 30C keeps the fan - // running until temp dips under lo. + // TX cooldown must not latch _thermal_on, or a TX at 30C keeps the fan + // running until the temperature dips under lo. if (t >= (float)_hi_c) _thermal_on = true; else if (t < (float)_lo_c) _thermal_on = false; - duty = _thermal_on ? 100 : 0; + enabled = _thermal_on || tx_cooling; } - if (_mode == FAN_AUTO && _manual_duty < 0 && !ntcImplausible(t)) { - if (tx_cooling && duty < FAN_TX_FLOOR_PCT) duty = FAN_TX_FLOOR_PCT; - } - - applyDuty(duty); + setFanOutputLocked(enabled); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::persistKey(const char* key, const char* value) { @@ -233,32 +217,28 @@ bool TBeam1WBoard::parseIntArg(const char* text, int& value) { void TBeam1WBoard::loadFanPrefs() { if (!_prefs) return; + FanMode mode = FAN_AUTO; char buf[12]; buf[0] = 0; if (_prefs->getByKey("fan", buf, 11)) { - if (strcmp(buf, "auto") == 0) _mode = FAN_AUTO; - else if (strcmp(buf, "off") == 0) _mode = FAN_OFF; - else if (strcmp(buf, "on") == 0) _mode = FAN_ON; + if (strcmp(buf, "off") == 0) mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) mode = FAN_ON; } - buf[0] = 0; - if (_prefs->getByKey("fan_drv", buf, 11)) { - if (strcmp(buf, "onoff") == 0) _drive = FAN_DRIVE_ONOFF; - else if (strcmp(buf, "pwm") == 0) _drive = FAN_DRIVE_PWM; - } - - int lo = _lo_c; - int hi = _hi_c; + int lo = FAN_DEFAULT_LO_C; + int hi = FAN_DEFAULT_HI_C; buf[0] = 0; if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); buf[0] = 0; if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + + portENTER_CRITICAL(&_fan_mux); + _mode = mode; if (lo >= 0 && hi <= 120 && lo < hi) { _lo_c = lo; _hi_c = hi; } - - _manual_duty = -1; + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { @@ -267,40 +247,44 @@ void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { updateFan(); } -const char* TBeam1WBoard::modeName() const { - if (_manual_duty >= 0) return "manual"; +const char* TBeam1WBoard::modeNameLocked() const { if (_mode == FAN_AUTO) return "auto"; if (_mode == FAN_OFF) return "off"; return "on"; } -const char* TBeam1WBoard::driveName() const { - return _drive == FAN_DRIVE_ONOFF ? "onoff" : "pwm"; -} - bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { (void)sender_timestamp; if (strcmp(command, "get fan") == 0) { - int cd = cooldownSecs(); - if (ntcImplausible(_temp_c)) { - sprintf(reply, "> %s n/a duty=%d%% %s cd=%ds", - modeName(), (int)_duty_pct, driveName(), cd); + portENTER_CRITICAL(&_fan_mux); + int cd = cooldownSecsLocked(); + float temp_c = _temp_c; + bool enabled = _fan_on; + const char* mode = modeNameLocked(); + portEXIT_CRITICAL(&_fan_mux); + if (ntcImplausible(temp_c)) { + sprintf(reply, "> %s n/a fan=%s cd=%ds", mode, enabled ? "on" : "off", cd); } else { - sprintf(reply, "> %s %.1fC duty=%d%% %s cd=%ds", - modeName(), (double)_temp_c, (int)_duty_pct, driveName(), cd); + sprintf(reply, "> %s %.1fC fan=%s cd=%ds", + mode, (double)temp_c, enabled ? "on" : "off", cd); } return true; } if (strncmp(command, "set fan.lo ", 11) == 0) { int lo; - if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= _hi_c || lo > 100) { + portENTER_CRITICAL(&_fan_mux); + int hi_limit = _hi_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= hi_limit || lo > 100) { strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); } else if (!persistKey("fan_lo", &command[11])) { strcpy(reply, "Error: failed to save fan.lo"); } else { + portENTER_CRITICAL(&_fan_mux); _lo_c = lo; + portEXIT_CRITICAL(&_fan_mux); sprintf(reply, "OK - fan.lo %d", lo); } return true; @@ -308,79 +292,51 @@ bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, if (strncmp(command, "set fan.hi ", 11) == 0) { int hi; - if (!parseIntArg(&command[11], hi) || hi <= _lo_c || hi > 120) { + portENTER_CRITICAL(&_fan_mux); + int lo_limit = _lo_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], hi) || hi <= lo_limit || hi > 120) { strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); } else if (!persistKey("fan_hi", &command[11])) { strcpy(reply, "Error: failed to save fan.hi"); } else { + portENTER_CRITICAL(&_fan_mux); _hi_c = hi; + portEXIT_CRITICAL(&_fan_mux); sprintf(reply, "OK - fan.hi %d", hi); } return true; } - if (strncmp(command, "set fan.drive ", 14) == 0) { - const char* arg = &command[14]; - if (strcmp(arg, "pwm") == 0) { - if (!persistKey("fan_drv", "pwm")) { - strcpy(reply, "Error: failed to save fan.drive"); - } else { - _drive = FAN_DRIVE_PWM; - applyDuty(_duty_pct); - strcpy(reply, "OK - fan.drive pwm"); - } - } else if (strcmp(arg, "onoff") == 0) { - if (!persistKey("fan_drv", "onoff")) { - strcpy(reply, "Error: failed to save fan.drive"); - } else { - _drive = FAN_DRIVE_ONOFF; - applyDuty(_duty_pct); - strcpy(reply, "OK - fan.drive onoff"); - } - } else { - strcpy(reply, "Error: fan.drive must be pwm or onoff"); - } - return true; - } - - if (strncmp(command, "set fan.duty ", 13) == 0) { - int duty; - if (!parseIntArg(&command[13], duty) || duty < 0 || duty > 100) { - strcpy(reply, "Error: fan.duty must be 0-100"); - } else { - _manual_duty = duty; - applyDuty(duty); - sprintf(reply, "OK - fan.duty %d (not saved)", duty); - } - return true; - } - if (strncmp(command, "set fan ", 8) == 0) { const char* arg = &command[8]; if (strcmp(arg, "on") == 0) { if (!persistKey("fan", "on")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_ON; - _manual_duty = -1; - applyDuty(100); + if (!_stopped) setFanOutputLocked(true); + portEXIT_CRITICAL(&_fan_mux); strcpy(reply, "OK - fan on"); } } else if (strcmp(arg, "off") == 0) { if (!persistKey("fan", "off")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_OFF; - _manual_duty = -1; - applyDuty(0); + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); strcpy(reply, "OK - fan off"); } } else if (strcmp(arg, "auto") == 0) { if (!persistKey("fan", "auto")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_AUTO; - _manual_duty = -1; + portEXIT_CRITICAL(&_fan_mux); updateFan(); strcpy(reply, "OK - fan auto"); } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index e96923c53..429b7bd1c 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -32,39 +32,34 @@ class TBeam1WBoard : public ESP32Board { public: enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; - enum FanDrive { FAN_DRIVE_PWM, FAN_DRIVE_ONOFF }; private: bool radio_powered = false; bool _stopped = false; - bool _pwm_attached = false; KeyValueStore* _prefs = nullptr; FanMode _mode = FAN_AUTO; - FanDrive _drive = FAN_DRIVE_ONOFF; int _lo_c = FAN_DEFAULT_LO_C; int _hi_c = FAN_DEFAULT_HI_C; - int _manual_duty = -1; // -1 = follow mode; 0..100 = CLI override bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this - volatile float _temp_c = NAN; - volatile int _duty_pct = 100; - volatile bool _tx_active = false; - volatile bool _tx_cooldown_active = false; - volatile uint32_t _tx_until_ms = 0; + bool _fan_on = true; + float _temp_c = NAN; + bool _tx_active = false; + bool _tx_cooldown_active = false; + uint32_t _tx_until_ms = 0; TaskHandle_t _fan_task = nullptr; + mutable portMUX_TYPE _fan_mux = portMUX_INITIALIZER_UNLOCKED; void startFanTask(); void updateFan(); - void applyDuty(int pct); + void setFanOutputLocked(bool enabled); float readNtcTempC(); - int rampDuty(float temp_c) const; - int cooldownSecs(); - bool isTxCooling(uint32_t now); + int cooldownSecsLocked(); + bool isTxCoolingLocked(uint32_t now); bool ntcImplausible(float temp_c) const; bool persistKey(const char* key, const char* value); static bool parseIntArg(const char* text, int& value); void loadFanPrefs(); - const char* modeName() const; - const char* driveName() const; + const char* modeNameLocked() const; static void fanTaskThunk(void* arg); public: diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index a67e2e846..528459a4c 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -85,16 +85,11 @@ #define NTC_R_FIXED 10000.0f #define NTC_VCC_MV 3300.0f -// Fan control (GPIO41). Default auto + on/off: NTC is PA-adjacent PCB temp, -// not die temp, so trip well below the SX1262/ESP32 85C operating limit. -// This fan/MOSFET path does not respond to PWM below 100% duty. +// Fan control (GPIO41). NTC is PA-adjacent PCB temp, not die temp, so trip +// well below the SX1262/ESP32 85C operating limit. Hardware testing confirmed +// that this fan/MOSFET path is on/off; PWM below 100% does not spin the fan. #define FAN_CTRL_PIN 41 -#define FAN_PWM_CHANNEL 4 -#define FAN_PWM_FREQ_HZ 25000 -#define FAN_PWM_RES_BITS 8 -#define FAN_MIN_DUTY_PCT 40 #define FAN_TX_COOLDOWN_MS 15000 -#define FAN_TX_FLOOR_PCT 100 #define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) #define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings From 62d0556a7024dd86046cea9d74e176f4737d91a0 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 5 Sep 2026 15:59:29 +1000 Subject: [PATCH 067/127] * added region load/save support * SUBSCRIBE now with timeout param (default 30 mins, max ~18 hours) --- examples/simple_sensor/SensorMesh.cpp | 102 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 7 +- src/helpers/ClientACL.h | 1 + 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 008cbcfc0..c3f2d9b07 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -322,8 +322,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - uint8_t reserved = payload[0]; + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint16_t timeout_secs; + memcpy(&timeout_secs, &payload[0], 2); + uint8_t reserved = payload[2]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -331,16 +333,19 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u r = region_map.getDefaultRegion(); } from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.min_deltas_len = payload[1]; + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + from->extra.sensor.min_deltas_len = payload[3]; // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); - getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness - strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[6]); + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.expiry_timestamp = 0; + // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness return 8; @@ -441,6 +446,25 @@ int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } +void SensorMesh::startRegionsLoad() { + temp_map.resetFrom(region_map); // rebuild regions in a temp instance + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; +} + +bool SensorMesh::saveRegions() { + return region_map.save(_fs); +} + +void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) { + if (r) { + region_map.getTransportKeysFor(*r, &default_scope, 1); + } else { + memset(default_scope.key, 0, sizeof(default_scope.key)); + } +} + uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { ClientInfo* client; if (data[0] == 0) { // blank password, just check if sender is in ACL @@ -454,7 +478,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } else { if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password #if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]); #endif return 0; } @@ -491,6 +515,40 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { + if (region_load_active) { + if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation + region_map = temp_map; // copy over the temp instance as new current map + region_load_active = false; + + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; + + char *ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name + + while (*ep && *ep != 'F') ep++; // look for (optional) flags + + if (indent > 0 && indent < 8 && strlen(np) > 0) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr + + load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's + } + } + } + reply[0] = 0; + } + return; + } + while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -548,15 +606,25 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char } else { // use default scope r = region_map.getDefaultRegion(); } - if (command[3] == ' ') { - // compile params as LPP data, eg. "sub 1:0.2V" - from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } else { - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + // defaults: + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + uint16_t timeout_secs = 30*60; // expires after 30 mins + if (command[3] == ' ') { // eg. "sub 300 1:0.2V" + char* cp = &command[4]; + while (*cp >= '0' && *cp <= '9') cp++; + if (cp > &command[4]) { + timeout_secs = atoi(&command[4]); + if (*cp == ' ') { + cp++; // skip the space + from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } + } } from->extra.sensor.scope_region_id = r ? r->id : 0; - if (from->extra.sensor.scope_region_id) { - strcpy(reply, "OK - subscribed"); + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + if (from->extra.sensor.expiry_timestamp) { + DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); + sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); } else { strcpy(reply, "Err - region scope needed"); } @@ -853,7 +921,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - region_map(key_store), + region_map(key_store), temp_map(key_store), _cli(board, rtc, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { @@ -864,6 +932,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; prev_telem_size = 0; + region_load_active = false; // defaults _prefs.airtime_factor = 1.0; @@ -1110,6 +1179,7 @@ void SensorMesh::loop() { if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope + if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6d5857d05..e0e03c81b 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -78,6 +78,9 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override { } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + void startRegionsLoad() override; + bool saveRegions() override; + void onDefaultRegionChanged(const RegionEntry* r) override; float getTelemValue(uint8_t channel, uint8_t type); @@ -148,8 +151,9 @@ private: uint8_t prev_telem_size; uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; - RegionMap region_map; + RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; + RegionEntry* load_stack[8]; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; @@ -160,6 +164,7 @@ private: float pending_bw; uint8_t pending_sf; uint8_t pending_cr; + bool region_load_active; bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 9fde35ecb..1e9f472d5 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -29,6 +29,7 @@ struct ClientInfo { uint8_t push_failures; } room; struct { + uint32_t expiry_timestamp; // epoch seconds uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From 0b2c47a932763daa06c395fe81391d59e95141f0 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:10:20 -0500 Subject: [PATCH 068/127] Add WiFi companion support for Pico W SerialWifiInterface has no ESP32-specific code, so move it to helpers/wifi and reuse it on RP2040. Guard the ESP32-only WiFi event/auto-reconnect calls and poll link state on RP2040 instead. --- examples/companion_radio/main.cpp | 24 ++++++++++----- platformio.ini | 1 + .../{esp32 => wifi}/SerialWifiInterface.cpp | 0 .../{esp32 => wifi}/SerialWifiInterface.h | 0 variants/rpi_picow/platformio.ini | 30 ++++++++++--------- 5 files changed, 34 insertions(+), 21 deletions(-) rename src/helpers/{esp32 => wifi}/SerialWifiInterface.cpp (100%) rename src/helpers/{esp32 => wifi}/SerialWifiInterface.h (100%) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 89f0e6cb9..7c8c12b9f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,9 +36,8 @@ MultiSerialInterface interface_manager; #ifndef TCP_PORT #define TCP_PORT 5000 #endif - #ifdef ESP32 - // include esp32 wifi interface - #include + #if defined(ESP32) || defined(RP2040_PLATFORM) + #include SerialWifiInterface wifi_interface; #else #error "SerialWifiInterface is not defined for this platform" @@ -108,7 +107,7 @@ void halt() { } /* WIFI RECONNECT TRACKERS */ -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; #endif @@ -192,6 +191,7 @@ void setup() { // add wifi interface #ifdef WIFI_SSID +#if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -204,6 +204,7 @@ void setup() { wifi_needs_reconnect = false; } }); +#endif WiFi.begin(WIFI_SSID, WIFI_PWD); wifi_interface.begin(TCP_PORT); @@ -262,12 +263,21 @@ void loop() { #endif } -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef WIFI_SSID + // RP2040 has no WiFi event callbacks, so poll the link state instead + #if defined(RP2040_PLATFORM) + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + #endif + // Safely attempt to reconnect every 10 seconds if flagged if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); - WiFi.disconnect(); - WiFi.reconnect(); + #if defined(RP2040_PLATFORM) + WiFi.begin(WIFI_SSID, WIFI_PWD); // no reconnect() on this platform + #else + WiFi.disconnect(); + WiFi.reconnect(); + #endif last_wifi_reconnect_attempt = millis(); } #endif diff --git a/platformio.ini b/platformio.ini index 2219c9786..622b01e27 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,6 +64,7 @@ build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} + + [esp32_ota] lib_deps = diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/wifi/SerialWifiInterface.cpp similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.cpp rename to src/helpers/wifi/SerialWifiInterface.cpp diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/wifi/SerialWifiInterface.h similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.h rename to src/helpers/wifi/SerialWifiInterface.h diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 0fe8c4369..32944a9a4 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -81,20 +81,22 @@ lib_ignore = BLE ; lib_deps = ${rpi_picow.lib_deps} ; densaugeo/base64 @ ~1.4.0 -; [env:PicoW_companion_radio_wifi] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D WIFI_DEBUG_LOGGING=1 -; -D WIFI_SSID='"myssid"' -; -D WIFI_PWD='"mypwd"' -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_wifi] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 +lib_ignore = BLE [env:PicoW_terminal_chat] extends = rpi_picow From 739a67c9f1504e029272a2fc68d241f1e9107846 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:18:51 -0500 Subject: [PATCH 069/127] Allow WiFi credentials to be set at runtime Store ssid/pwd in NodePrefs and set them with 'set wifi.ssid' / 'set wifi.pwd' over USB serial; build-time WIFI_SSID/WIFI_PWD stay as the fallback. Headless WiFi builds get the config CLI on Serial, which is otherwise unused there. --- examples/companion_radio/MyMesh.cpp | 34 ++++++++++++++++++++++++++++ examples/companion_radio/NodePrefs.h | 27 +++++++++++++++++++++- examples/companion_radio/main.cpp | 13 +++++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ee8114ca9..8550890e1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -932,6 +932,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { _iter_started = false; _cli_rescue = false; + cli_command[0] = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -2156,6 +2157,35 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } +#ifdef WIFI_SSID + // local console only: these are credentials, and remote admin has no business with them + if (sender_timestamp == 0) { + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); + savePrefs(); + sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); + savePrefs(); + strcpy(reply, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "set wifi.clear") == 0) { + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + savePrefs(); + strcpy(reply, "> wifi config cleared, using build-time credentials (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "(build-time)"); + return true; + } + } +#endif + if (strcmp(command, "board") == 0) { strcpy(reply, board.getManufacturerName()); return true; @@ -2386,6 +2416,10 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); +#if defined(WIFI_SSID) && !defined(ENABLE_USB_INTERFACE) + // headless WiFi build: USB serial isn't a companion transport, so use it for config + checkCLIRescueCmd(); +#endif } // is there are pending dirty contacts write needed? diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f6f9b887c..c725c317a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -44,6 +44,10 @@ public: char default_scope_name[31]; uint8_t default_scope_key[16]; int8_t tz_offset = 0; +#ifdef WIFI_SSID + char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used + char wifi_pwd[64] = {0}; +#endif private: class RadioPrefs : public CommonRadioPrefs { @@ -160,6 +164,20 @@ private: DynamicConfigSerializer custom; +#ifdef WIFI_SSID + class WiFiPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid)); + def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd)); + } + public: + WiFiPrefs(NodePrefs* parent) : _parent(parent) { } + }; + WiFiPrefs wifi; +#endif + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -172,9 +190,16 @@ protected: def("repeat", repeat); def("comp", companion); def("custom", custom); +#ifdef WIFI_SSID + def("wifi", wifi); +#endif } public: - NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) { + NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) +#ifdef WIFI_SSID + , wifi(this) +#endif + { node_name[0] = 0; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 7c8c12b9f..64aa3d269 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -110,6 +110,8 @@ void halt() { #ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; + const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set + const char* wifi_pwd = WIFI_PWD; #endif void setup() { @@ -206,7 +208,14 @@ void setup() { }); #endif - WiFi.begin(WIFI_SSID, WIFI_PWD); + // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial) + if (the_mesh.getNodePrefs()->wifi_ssid[0]) { + wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; + wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; + } + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + + WiFi.begin(wifi_ssid, wifi_pwd); wifi_interface.begin(TCP_PORT); interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif @@ -273,7 +282,7 @@ void loop() { if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); #if defined(RP2040_PLATFORM) - WiFi.begin(WIFI_SSID, WIFI_PWD); // no reconnect() on this platform + WiFi.begin(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); WiFi.reconnect(); From 58181bb8a285d7c1658dc0c4908232440ebdf829 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:33:40 -0500 Subject: [PATCH 070/127] Fix blocking WiFi connect on RP2040, log link state arduino-pico's WiFi.begin() blocks for up to 2x its 15s timeout, which stalled the mesh loop on every reconnect attempt; use beginNoBlock(). Log the IP when the link comes up, and the status code when retrying. --- examples/companion_radio/main.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 64aa3d269..f0c12908f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -112,6 +112,7 @@ void halt() { unsigned long last_wifi_reconnect_attempt = 0; const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set const char* wifi_pwd = WIFI_PWD; + bool wifi_was_connected = false; #endif void setup() { @@ -215,7 +216,11 @@ void setup() { } WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); +#if defined(RP2040_PLATFORM) + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // begin() blocks for up to 2x its 15s timeout +#else WiFi.begin(wifi_ssid, wifi_pwd); +#endif wifi_interface.begin(TCP_PORT); interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif @@ -276,13 +281,21 @@ void loop() { // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + wifi_was_connected = !wifi_needs_reconnect; + if (wifi_was_connected) { + WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); + } else { + WIFI_DEBUG_PRINTLN("link lost"); + } + } #endif // Safely attempt to reconnect every 10 seconds if flagged if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) - WiFi.begin(wifi_ssid, wifi_pwd); // no reconnect() on this platform + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); WiFi.reconnect(); From a4ac85a0d6b3d392f303e054ab03516933dbec52 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:57:01 -0500 Subject: [PATCH 071/127] Address review findings on WiFi companion support - scope the serial config CLI to RP2040; it was exposing the rescue CLI (cat/rm/erase) on every ESP32 WiFi build, which gates it behind a physical long-press - bound and space out RP2040 rejoins: the core's join busy-waits, so cap it at 5s and retry every 30s instead of every 10s - stamp the reconnect timer in setup(), so the first loop() doesn't tear down an association that is still finishing DHCP - treat stored credentials as a pair, and pass NULL (not "") for an open network - teach build_as_lib.py where SerialWifiInterface moved --- build_as_lib.py | 2 ++ examples/companion_radio/MyMesh.cpp | 5 +++-- examples/companion_radio/main.cpp | 24 ++++++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/build_as_lib.py b/build_as_lib.py index d8e95378e..fbc7c15a7 100644 --- a/build_as_lib.py +++ b/build_as_lib.py @@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []): src_filter.append("+") elif item == "ESP32": src_filter.append("+") + src_filter.append("+") elif item == "NRF52_PLATFORM": src_filter.append("+") elif item == "RP2040_PLATFORM": src_filter.append("+") + src_filter.append("+") # DISPLAY HANDLING elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS": diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 8550890e1..a8e305a6a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2416,8 +2416,9 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); -#if defined(WIFI_SSID) && !defined(ENABLE_USB_INTERFACE) - // headless WiFi build: USB serial isn't a companion transport, so use it for config +#if defined(WIFI_SSID) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) + // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a + // display + long-press), so serve config commands on the otherwise unused USB serial checkCLIRescueCmd(); #endif } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index f0c12908f..ff0794ab9 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,6 +36,12 @@ MultiSerialInterface interface_manager; #ifndef TCP_PORT #define TCP_PORT 5000 #endif + #ifndef WIFI_RETRY_INTERVAL + #define WIFI_RETRY_INTERVAL 30000 // millis between reconnect attempts + #endif + #ifndef WIFI_RETRY_TIMEOUT + #define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop() + #endif #if defined(ESP32) || defined(RP2040_PLATFORM) #include SerialWifiInterface wifi_interface; @@ -209,15 +215,23 @@ void setup() { }); #endif - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial) + // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). + // they are taken as a pair, so 'set wifi.ssid' alone gives an open-network join, not a + // silent fallback to the build-time password of a different network. if (the_mesh.getNodePrefs()->wifi_ssid[0]) { wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; } + if (wifi_pwd[0] == 0) wifi_pwd = NULL; // NULL (not "") selects an open network WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // begin() blocks for up to 2x its 15s timeout + // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the + // extra DHCP wait. Give the first connect a full window, then bound the retries below. + // Upgrade path if the stall ever matters: run WiFi on core1. + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); + last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else WiFi.begin(wifi_ssid, wifi_pwd); #endif @@ -291,10 +305,12 @@ void loop() { } #endif - // Safely attempt to reconnect every 10 seconds if flagged - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { + // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop + // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); From d33fb4e9a7f61b0d25d30a2c78feed05dee42496 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sat, 5 Sep 2026 21:15:12 -0500 Subject: [PATCH 072/127] Fix config parser aborting on an empty object 'custom:{}' (a DynamicConfigSerializer with nothing set) hits EXPECT_KEY with a '}' and returns TOK_ERROR, so loadSerial stops there and silently drops every property after it. Nothing follows 'custom' in NodePrefs today, so it goes unnoticed until you add one. Also include stdlib.h, which Arduino.h was providing on-device but not in the native test build. --- src/helpers/ConfigSerializer.cpp | 2 + .../test_config_serializer.cpp | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index adff147f4..36aff5ccf 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,4 +1,5 @@ #include "ConfigSerializer.h" +#include // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not) bool ConfigSerializer::saveSerial(Stream& s) { Context context(&s, OP::WRITE); @@ -62,6 +63,7 @@ int ConfigSerializer::Context::readNext() { case EXPECT_COMMA_OR_KEY: if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } case EXPECT_KEY: + if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}' if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index dec554830..80d5e7808 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -185,6 +185,47 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } +class TestNested : public ConfigSerializer { + class Inner : public ConfigSerializer { + protected: + void structure() override { } // no properties, so it writes as '{}' + }; + Inner inner; + protected: + void structure() override { + def("age", age); + def("inner", inner); + def("name", name, sizeof(name)); // comes *after* the empty sub-object + } + public: + int32_t age; + char name[16]; +}; + +TEST(ConfigSerializer, LoadSerial_EmptyObject) { + MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); // properties after an empty object must still load +} + +TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) { + MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + TEST(DynamicConfigSerializer, GetSet_Basic) { DynamicConfigSerializer data; From 5d82ed352c2481f5ec36e4b8e8ab57c963cb6c20 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 6 Sep 2026 15:14:35 +0200 Subject: [PATCH 073/127] Companion CLI: fixed txdelay, direct.txdelay, agc.reset.interval, int.thresh --- examples/companion_radio/MyMesh.cpp | 10 +++++++--- examples/companion_radio/MyMesh.h | 3 +++ examples/companion_radio/NodePrefs.h | 28 ++++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f5a4a5a92..53e6f4d54 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -261,7 +261,7 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return _prefs.interference_threshold; } bool MyMesh::getCADEnabled() const { return _prefs.cad_enabled; @@ -273,11 +273,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } @@ -890,6 +890,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe // defaults _prefs.airtime_factor = 1.0; + _prefs.tx_delay_factor = 0.5f; + _prefs.direct_tx_delay_factor = 0.2f; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; _prefs.sf = LORA_SF; @@ -952,6 +954,8 @@ void MyMesh::begin(bool has_display) { // sanitise bad pref values _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + _prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f); + _prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f); _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); _prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f); _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 780de35dd..4aab11cd6 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -106,6 +106,9 @@ protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; bool getCADEnabled() const override; + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } int calcRxDelay(float score, uint32_t air_time) const override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c8a9ab830..4581185e0 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -27,6 +27,8 @@ public: uint8_t telemetry_mode_loc = 0; uint8_t telemetry_mode_env = 0; float rx_delay_base = 0; + float tx_delay_factor = 0; + float direct_tx_delay_factor = 0; uint32_t ble_pin = 0; uint8_t advert_loc_policy = 0; uint8_t buzzer_quiet = 0; @@ -41,6 +43,8 @@ public: uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) uint8_t cad_enabled = 0; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // secs / 4 char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -54,16 +58,16 @@ private: def("sf", _parent->sf); def("cr", _parent->cr); def("cad", _parent->cad_enabled); - //def("int_thr", _parent->interference_threshold); + def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously def("fem_txgain", _parent->radio_fem_txgain); def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); - //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded - //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded - //def("agc_int", _parent->agc_reset_interval); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); def("hash_mode", _parent->path_hash_mode); def("multi_ack", _parent->multi_acks); } @@ -83,24 +87,24 @@ private: void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } - uint8_t getIntThresh() const override { return 0; } - void setIntThresh(uint8_t t) override { /* no-op */ } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } uint8_t getTxPower() const override { return _parent->tx_power_dbm; } void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return 0; } - void setAgcResetInt(uint8_t secs) override { /* no-op */ } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } - float getFloodTxDelay() const override { return 0.5f; } // currently hard-coded - void setFloodTxDelay(float d) override { /* no-op */ } - float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded - void setDirectTxDelay(float d) override { /* no-op */ } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } From 88cc014f55d07470ec906dcdb5163be9695f06d2 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 20:15:45 -0500 Subject: [PATCH 074/127] Add BLE companion support for Pico W --- examples/companion_radio/main.cpp | 4 + src/helpers/rp2040/SerialBLEInterface.cpp | 198 ++++++++++++++++++++++ src/helpers/rp2040/SerialBLEInterface.h | 75 ++++++++ variants/rpi_picow/platformio.ini | 50 ++++-- 4 files changed, 314 insertions(+), 13 deletions(-) create mode 100644 src/helpers/rp2040/SerialBLEInterface.cpp create mode 100644 src/helpers/rp2040/SerialBLEInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ff0794ab9..85923a152 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -26,6 +26,10 @@ MultiSerialInterface interface_manager; // include nrf52 bluetooth interface #include SerialBLEInterface bluetooth_interface; + #elif defined(RP2040_PLATFORM) + // include rp2040 (Pico W / CYW43) bluetooth interface + #include + SerialBLEInterface bluetooth_interface; #else #error "SerialBLEInterface is not defined for this platform" #endif diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp new file mode 100644 index 000000000..f03210039 --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -0,0 +1,198 @@ +#include "SerialBLEInterface.h" +#include +#include +#include + +// Nordic UART 6E4000xx-B5A3-F393-E0A9-E50E24DCCA9E, as raw bytes: the core lib's string +// parser uses sscanf("%llx"), which newlib-nano on the RP2040 doesn't support (yields all zeros) +#define NUS_UUID(n) { 0x6E, 0x40, 0x00, n, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E } +static const uint8_t SERVICE_UUID[16] = NUS_UUID(0x01); +static const uint8_t CHARACTERISTIC_UUID_RX[16] = NUS_UUID(0x02); +static const uint8_t CHARACTERISTIC_UUID_TX[16] = NUS_UUID(0x03); + +// The BLE core lib's own setValue()/notify path reallocs the value buffer on every call, +// so a second frame queued before the radio drained the first would corrupt it. We keep our +// own frame queue and drive att_server_notify() from the can-send-now callback instead. + +SerialBLEInterface::SerialBLEInterface() + : BLEService(BLEUUID(SERVICE_UUID)), + _rx(BLEUUID(CHARACTERISTIC_UUID_RX), BLEWrite, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED), + _tx(BLEUUID(CHARACTERISTIC_UUID_TX), BLERead | BLENotify, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED) +{ + _isEnabled = false; + _tx_pending = false; + send_queue_len = 0; + recv_queue_len = 0; + memset(&_can_send, 0, sizeof(_can_send)); + _rx.setCallbacks(this); + addCharacteristic(&_rx); + addCharacteristic(&_tx); +} + +void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { + // JustWorks here only makes the server request pairing on connect; caps are overridden below + BLE.setSecurity(BLESecurityJustWorks); + // adv data carries the 128-bit service UUID, leaving room for only 8 name chars; + // the full name goes in the scan response + BLE.begin(prefix); + + if (strcmp(name, "@@MAC") == 0) { + bd_addr_t a; + gap_local_bd_addr(a); + sprintf(name, "%02X%02X%02X%02X%02X%02X", a[0], a[1], a[2], a[3], a[4], a[5]); // modify (IN-OUT param) + } + char dev_name[32+16]; + snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, name); + + BLE.server()->setName(dev_name); // GAP device name characteristic + BLE.server()->addService(this); + BLE.server()->setCallbacks(this); + + // static passkey pairing with MITM protection, matching the esp32/nrf52 interfaces + sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); + sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION | SM_AUTHREQ_BONDING); + sm_use_fixed_passkey_in_display_role(pin_code); + + size_t n = strlen(dev_name); + if (n > sizeof(_scan_rsp) - 2) n = sizeof(_scan_rsp) - 2; + _scan_rsp[0] = n + 1; + _scan_rsp[1] = BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME; + memcpy(&_scan_rsp[2], dev_name, n); + gap_scan_response_set_data(n + 2, _scan_rsp); + + BLE_DEBUG_PRINTLN("begin: name=%s", dev_name); +} + +void SerialBLEInterface::clearBuffers() { + send_queue_len = 0; + recv_queue_len = 0; + _tx_pending = false; // a stale registration just fires into an empty queue; BTstack ignores double-adds +} + +void SerialBLEInterface::onConnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("connected handle=0x%04X", _tx.conHandle()); + clearBuffers(); +} + +void SerialBLEInterface::onDisconnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("disconnected"); + clearBuffers(); // BTstack re-enables advertising on its own +} + +void SerialBLEInterface::onWrite(BLECharacteristic* c) { + if (c != &_rx) return; + size_t len = _rx.valueLen(); + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: bad frame len=%u", (unsigned)len); + return; + } + if (recv_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: recv queue full, dropping frame"); + return; + } + recv_queue[recv_queue_len].len = len; + memcpy(recv_queue[recv_queue_len].buf, _rx.valueData(), len); + recv_queue_len++; +} + +// caller holds the BT lock (or is in the BT context) +void SerialBLEInterface::kickSend() { + if (_tx_pending) return; + _tx_pending = true; + _can_send.callback = onCanSend; + _can_send.context = this; + if (att_server_register_can_send_now_callback(&_can_send, _tx.conHandle()) != 0) { + _tx_pending = false; + } +} + +// BT context: one notification per can-send-now, re-arm while frames remain +void SerialBLEInterface::sendNext() { + _tx_pending = false; + if (send_queue_len == 0) return; + if (!isConnected()) { + BLE_DEBUG_PRINTLN("sendNext: not connected, clearing send queue"); + send_queue_len = 0; + return; + } + uint16_t h = _tx.conHandle(); + Frame& f = send_queue[0]; + uint16_t mtu = att_server_get_mtu(h); + if (f.len + 3 > mtu) { + // att would silently truncate; drop instead (client must negotiate MTU >= MAX_FRAME_SIZE+3) + BLE_DEBUG_PRINTLN("sendNext: frame len=%u exceeds mtu=%u, dropping", f.len, mtu); + } else { + uint8_t err = att_server_notify(h, _tx.valueHandle(), f.buf, f.len); + if (err == BTSTACK_ACL_BUFFERS_FULL) { + kickSend(); + return; + } + if (err) { + BLE_DEBUG_PRINTLN("sendNext: notify failed err=%u, dropping", err); + } else { + BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", f.len, f.buf[0]); + } + } + send_queue_len--; + memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame)); + if (send_queue_len > 0) kickSend(); +} + +void SerialBLEInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); + BLE.startAdvertising(true); +} + +void SerialBLEInterface::disconnect() { + uint16_t h = _tx.conHandle(); + if (h) gap_disconnect(h); +} + +void SerialBLEInterface::disable() { + _isEnabled = false; + BLE_DEBUG_PRINTLN("disable"); + disconnect(); + BLE.stopAdvertising(); +} + +bool SerialBLEInterface::isConnected() const { + // notifications can only be enabled once the link is authenticated (CCCD inherits the perms) + return _isEnabled && _tx.conHandle() != 0 && _tx.notifyEnabled(); +} + +bool SerialBLEInterface::isWriteBusy() const { + return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); +} + +size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len); + return 0; + } + if (!isConnected()) return 0; + + BluetoothLock lock; + if (send_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + send_queue[send_queue_len].len = len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + kickSend(); + return len; +} + +size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { + BluetoothLock lock; + if (recv_queue_len == 0) return 0; + + size_t len = recv_queue[0].len; + memcpy(dest, recv_queue[0].buf, len); + recv_queue_len--; + memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame)); + BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]); + return len; +} diff --git a/src/helpers/rp2040/SerialBLEInterface.h b/src/helpers/rp2040/SerialBLEInterface.h new file mode 100644 index 000000000..2e085a6cc --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.h @@ -0,0 +1,75 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include +#include + +// Nordic UART service over the arduino-pico BLE library (BTstack on the CYW43). +// Build with -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH so the core links liblwip-bt. +class SerialBLEInterface : public BaseSerialInterface, BLEService, BLEServerCallbacks, BLECharacteristicCallbacks { + // subclass only to reach the protected connection/notify state + struct Characteristic : public BLECharacteristic { + using BLECharacteristic::BLECharacteristic; + uint16_t valueHandle() const { return _valueHandle; } + uint16_t conHandle() const { return con_handle; } + bool notifyEnabled() const { return _notificationEnabled; } + }; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 8 + + Characteristic _rx; + Characteristic _tx; + bool _isEnabled; + bool _tx_pending; // a can-send-now callback is registered + btstack_context_callback_registration_t _can_send; + uint8_t _scan_rsp[31]; // complete local name; BTstack keeps the pointer + + uint8_t send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + uint8_t recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers(); + void kickSend(); + void sendNext(); + static void onCanSend(void* ctx) { ((SerialBLEInterface*)ctx)->sendNext(); } + + // BLE library callbacks (run in the BT context) + void onWrite(BLECharacteristic* c) override; + void onConnect(BLEServer* s) override; + void onDisconnect(BLEServer* s) override; + +public: + SerialBLEInterface(); + + /** + * init the BLE interface. + * @param prefix a prefix for the device name + * @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned + * @param pin_code the BLE security pin + */ + void begin(const char* prefix, char* name, uint32_t pin_code); + + void disconnect(); + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + bool isConnected() const override; + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if BLE_DEBUG_LOGGING && ARDUINO + #include + #define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__) + #define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__) +#else + #define BLE_DEBUG_PRINT(...) {} + #define BLE_DEBUG_PRINTLN(...) {} +#endif diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 32944a9a4..c41806505 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -67,19 +67,43 @@ lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 lib_ignore = BLE -; [env:PicoW_companion_radio_ble] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_ble] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; USB + WiFi + BLE together; the interface manager fans frames out to all of them +[env:PicoW_companion_radio_all] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 [env:PicoW_companion_radio_wifi] extends = rpi_picow From b9fa450f52512486ce4b19bd09b71c383cfb2e29 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 20:43:37 -0500 Subject: [PATCH 075/127] Fix review findings on Pico W WiFi/BLE --- examples/companion_radio/MyMesh.cpp | 3 ++- examples/companion_radio/main.cpp | 21 +++++++++++++-------- src/helpers/rp2040/SerialBLEInterface.cpp | 3 +++ variants/rpi_picow/platformio.ini | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a8e305a6a..4eea0b2fb 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2158,7 +2158,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } #ifdef WIFI_SSID - // local console only: these are credentials, and remote admin has no business with them + // not accepted from the remote mesh CLI (timestamp != 0): these are credentials. The app + // over USB/BLE/WiFi and the serial console (timestamp 0) may set them. if (sender_timestamp == 0) { if (memcmp(command, "set wifi.ssid ", 14) == 0) { StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 85923a152..6b6d7cf74 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -41,7 +41,11 @@ MultiSerialInterface interface_manager; #define TCP_PORT 5000 #endif #ifndef WIFI_RETRY_INTERVAL - #define WIFI_RETRY_INTERVAL 30000 // millis between reconnect attempts + #if defined(RP2040_PLATFORM) + #define WIFI_RETRY_INTERVAL 30000 // each attempt blocks loop(), so retry less often + #else + #define WIFI_RETRY_INTERVAL 10000 // millis between reconnect attempts + #endif #endif #ifndef WIFI_RETRY_TIMEOUT #define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop() @@ -120,8 +124,8 @@ void halt() { #ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; - const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set - const char* wifi_pwd = WIFI_PWD; + char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set + char wifi_pwd[64] = WIFI_PWD; bool wifi_was_connected = false; #endif @@ -220,13 +224,14 @@ void setup() { #endif // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). - // they are taken as a pair, so 'set wifi.ssid' alone gives an open-network join, not a - // silent fallback to the build-time password of a different network. + // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a + // silent fallback to the build-time password of a different network. Copied out of prefs + // so 'set wifi.*' edits only take effect on reboot, as their replies promise. + // (No NULL-for-open-network: the RP2040 core does strlen() on the password unguarded.) if (the_mesh.getNodePrefs()->wifi_ssid[0]) { - wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; - wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; + strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); + strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - if (wifi_pwd[0] == 0) wifi_pwd = NULL; // NULL (not "") selects an open network WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp index f03210039..a1fa487e7 100644 --- a/src/helpers/rp2040/SerialBLEInterface.cpp +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -1,3 +1,5 @@ +// only built when the env enables the core BLE stack (build_as_lib.py globs this dir) +#ifdef PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH #include "SerialBLEInterface.h" #include #include @@ -196,3 +198,4 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]); return len; } +#endif diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index c41806505..f99b13417 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -92,8 +92,8 @@ build_flags = ${rpi_picow.build_flags} -D ENABLE_USB_INTERFACE -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 - -D WIFI_DEBUG_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) +; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' ; -D MESH_PACKET_LOGGING=1 From b11a779843fc949ec7589de4fc7fba2f84750d25 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:28:04 -0500 Subject: [PATCH 076/127] Address review: per-variant wifi src, real SSID reply, rename PicoW env --- examples/companion_radio/MyMesh.cpp | 2 +- platformio.ini | 1 - variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/lilygo_tbeam_1w/platformio.ini | 1 + variants/lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/meshnology_w12/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 2 +- variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 24 files changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4eea0b2fb..e835d8ef4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2181,7 +2181,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "(build-time)"); + sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } } diff --git a/platformio.ini b/platformio.ini index 622b01e27..2219c9786 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,7 +64,6 @@ build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} - + [esp32_ota] lib_deps = diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 354004f07..df986cf0c 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -185,6 +185,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -324,6 +325,7 @@ build_flags = build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d040b72f9..178508d88 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -195,6 +195,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 78561a14a..28e805543 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -189,6 +189,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 65e636eb4..27541a8a5 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -198,6 +198,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -350,6 +351,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index d718c006c..25f9ee3bb 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -241,6 +241,7 @@ build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -406,6 +407,7 @@ build_src_filter = ${heltec_v4_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index f523ebf9b..8c2feb946 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -186,6 +186,7 @@ build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -311,6 +312,7 @@ build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 0f604fac4..16db0257d 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -165,6 +165,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 8bfc4093a..a9991f253 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -160,6 +160,7 @@ build_flags = ; -D CORE_DEBUG_LEVEL=4 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 1aea74d28..cf24a5d53 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=128 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 8255e46cb..c0e8c80d9 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -184,6 +184,7 @@ build_src_filter = ${meshnology_w12.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 112181df2..3c5049e06 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -154,6 +154,7 @@ build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 1161743ea..9789eabf8 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -150,6 +150,7 @@ build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 8bd3c5977..2fee8c264 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -182,6 +182,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index f99b13417..e54d86a64 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -84,7 +84,7 @@ lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 ; USB + WiFi + BLE together; the interface manager fans frames out to all of them -[env:PicoW_companion_radio_all] +[env:PicoW_companion_radio] extends = rpi_picow build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index bdb7ee0c3..d8bdd5e81 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -234,6 +234,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 074d6a2ed..477c135b6 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -149,6 +149,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index 583f913c9..c1f5612a3 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -184,6 +184,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 5e85b6498..f64e4e774 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -198,6 +198,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M5.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 7d9892e5f..508fc8139 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -145,6 +145,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 09b1391d6..085ab7d51 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -146,6 +146,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c9c107c68..cca2907a5 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -115,6 +115,7 @@ extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + + + build_flags = ${Xiao_esp32_C3.build_flags} -D MAX_CONTACTS=350 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 293a13c15..de656c44d 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -202,6 +202,7 @@ extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> build_flags = From 98cf4f3332b8f537e36979c1ccff1f6e12766048 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:44:47 -0500 Subject: [PATCH 077/127] Add get wifi.status and get wifi.ip CLI commands --- examples/companion_radio/MyMesh.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e835d8ef4..76e0dfde9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,6 +1,9 @@ #include "MyMesh.h" #include // needed for PlatformIO +#ifdef WIFI_SSID +#include +#endif #include #define CMD_APP_START 1 @@ -2184,6 +2187,18 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } + if (strcmp(command, "get wifi.status") == 0) { + strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); + } else { + strcpy(reply, "> (not connected)"); + } + return true; + } } #endif From eb2ab10de0a23ab76e79379ee09109260c9f4efa Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:56:25 -0500 Subject: [PATCH 078/127] Add wifi.enabled pref and CLI; skip WiFi when disabled or SSID blank --- examples/companion_radio/MyMesh.cpp | 10 +++ examples/companion_radio/NodePrefs.h | 2 + examples/companion_radio/main.cpp | 95 +++++++++++++++------------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 76e0dfde9..2886ceb69 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2187,6 +2187,16 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + savePrefs(); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + sprintf(reply, "> %d", _prefs.wifi_enabled); + return true; + } if (strcmp(command, "get wifi.status") == 0) { strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); return true; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c725c317a..033236750 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,6 +47,7 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; + uint8_t wifi_enabled = 1; // 0 = never bring up WiFi (credentials may still be baked in) #endif private: @@ -171,6 +172,7 @@ private: void structure() override { def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid)); def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd)); + def("enabled", _parent->wifi_enabled); } public: WiFiPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6b6d7cf74..cf7610958 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -127,6 +127,7 @@ void halt() { char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set char wifi_pwd[64] = WIFI_PWD; bool wifi_was_connected = false; + bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank #endif void setup() { @@ -208,21 +209,6 @@ void setup() { // add wifi interface #ifdef WIFI_SSID -#if defined(ESP32) - board.setInhibitSleep(true); // prevent sleep when WiFi is active - WiFi.setAutoReconnect(true); - - WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ - if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); - wifi_needs_reconnect = true; - } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { - WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); - wifi_needs_reconnect = false; - } - }); -#endif - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a // silent fallback to the build-time password of a different network. Copied out of prefs @@ -232,20 +218,41 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + // 'set wifi.enabled 0' or a build with blank credentials leaves the radio off entirely + wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled && wifi_ssid[0]; + if (wifi_enabled) { +#if defined(ESP32) + board.setInhibitSleep(true); // prevent sleep when WiFi is active + WiFi.setAutoReconnect(true); + + WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + wifi_needs_reconnect = true; + } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { + WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + wifi_needs_reconnect = false; + } + }); +#endif + + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the - // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the - // extra DHCP wait. Give the first connect a full window, then bound the retries below. - // Upgrade path if the stall ever matters: run WiFi on core1. - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); - last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry + // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the + // extra DHCP wait. Give the first connect a full window, then bound the retries below. + // Upgrade path if the stall ever matters: run WiFi on core1. + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); + last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else - WiFi.begin(wifi_ssid, wifi_pwd); + WiFi.begin(wifi_ssid, wifi_pwd); #endif - wifi_interface.begin(TCP_PORT); - interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + } else { + WIFI_DEBUG_PRINTLN("wifi disabled"); + } #endif // add usb interface @@ -301,31 +308,33 @@ void loop() { } #ifdef WIFI_SSID - // RP2040 has no WiFi event callbacks, so poll the link state instead + if (wifi_enabled) { + // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) - wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); - if (wifi_was_connected == wifi_needs_reconnect) { // link state changed - wifi_was_connected = !wifi_needs_reconnect; - if (wifi_was_connected) { - WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); - } else { - WIFI_DEBUG_PRINTLN("link lost"); + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + wifi_was_connected = !wifi_needs_reconnect; + if (wifi_was_connected) { + WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); + } else { + WIFI_DEBUG_PRINTLN("link lost"); + } } - } #endif - // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop - // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); + // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop + // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) - WiFi.setTimeout(WIFI_RETRY_TIMEOUT); - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else - WiFi.disconnect(); - WiFi.reconnect(); + WiFi.disconnect(); + WiFi.reconnect(); #endif - last_wifi_reconnect_attempt = millis(); + last_wifi_reconnect_attempt = millis(); + } } #endif } From dac171b49c23ab11a9906a80f55ea0e5d14c9b0f Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 22:05:29 -0500 Subject: [PATCH 079/127] Default wifi.enabled to 0 --- examples/companion_radio/NodePrefs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 033236750..68facfbec 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,7 +47,7 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 1; // 0 = never bring up WiFi (credentials may still be baked in) + uint8_t wifi_enabled = 0; // off until 'set wifi.enabled 1' (credentials may still be baked in) #endif private: From 7c2b4d88be891d884907875172dfc99beb1c48b0 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 22:18:00 -0500 Subject: [PATCH 080/127] WiFi on/off tri-state: on when SSID set unless explicitly disabled; PicoW ships no default SSID --- examples/companion_radio/MyMesh.cpp | 13 +++++++++---- examples/companion_radio/NodePrefs.h | 7 ++++++- examples/companion_radio/main.cpp | 4 ++-- variants/rpi_picow/platformio.ini | 8 ++++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2886ceb69..b2c992882 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2184,17 +2184,22 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); + sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { - _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + uint8_t en = atoi(&command[17]) ? 1 : 0; + if (en && !_prefs.wifiSSID()[0]) { + strcpy(reply, "> set wifi.ssid first"); + return true; + } + _prefs.wifi_enabled = en; savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", en); return true; } if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifi_enabled); + sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); return true; } if (strcmp(command, "get wifi.status") == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 68facfbec..85cdebb2d 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,7 +47,12 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 0; // off until 'set wifi.enabled 1' (credentials may still be baked in) + uint8_t wifi_enabled = 2; // 0 = off, 1 = on, 2 = never set (treated as on) + + // effective SSID: stored prefs win over the build-time one + const char* wifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } + // WiFi runs only when there is an SSID and it hasn't been explicitly turned off + bool wifiEnabled() const { return wifiSSID()[0] && wifi_enabled != 0; } #endif private: diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index cf7610958..d461a36d0 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -218,8 +218,8 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - // 'set wifi.enabled 0' or a build with blank credentials leaves the radio off entirely - wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled && wifi_ssid[0]; + // 'set wifi.enabled 0', or no SSID from either prefs or the build, leaves the radio off entirely + wifi_enabled = the_mesh.getNodePrefs()->wifiEnabled(); if (wifi_enabled) { #if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index e54d86a64..763c96bc2 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -94,8 +94,8 @@ build_flags = ${rpi_picow.build_flags} -D BLE_PIN_CODE=123456 ; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) ; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='"myssid"' - -D WIFI_PWD='"mypwd"' + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} @@ -111,8 +111,8 @@ build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='"myssid"' - -D WIFI_PWD='"mypwd"' + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} From d71a54cd3dfe8dc5eca7c0e0834c6e6e3ea18837 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Sep 2026 13:56:47 +1000 Subject: [PATCH 081/127] * CLI: removed "sub" and "unsub" (cannot support via command line) * _SUBSCRIBE: binary req/resp payload changes (now keeps supplied 'push_tag') * Sensor: added MCU temperature support in telemetry --- examples/simple_sensor/SensorMesh.cpp | 112 ++++++-------------------- src/helpers/ClientACL.h | 1 + 2 files changed, 27 insertions(+), 86 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index c3f2d9b07..8c6b06a6d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,8 +55,10 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 -#define REQ_TYPE_SUBSCRIBE 0x10 -#define REQ_TYPE_UNSUBSCRIBE 0x11 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present) + +#define REQ_TYPE_SUBSCRIBE 0x08 +#define REQ_TYPE_UNSUBSCRIBE 0x09 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -193,43 +195,6 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } -static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { - const char* parts[3]; - int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); - uint8_t len = 0; - for (int i = 0; i < n && len + 6 <= max_len; i++) { - const char* cp = strchr(parts[i], ':'); - if (cp) { - uint8_t t; - float factor = 1.0f; - cp++; // skip the ':' - char* ep = strchr(cp, 0) - 1; // find LAST char - if (*ep == 'V') { // Volts - t = LPP_VOLTAGE; - } else if (*ep == 'W') { // Watts - t = LPP_POWER; - } else if (*ep == 'C') { // Celcius - t = LPP_TEMPERATURE; - } else if (*ep == 'P') { // Pascals - t = LPP_BAROMETRIC_PRESSURE; - } else if (*ep == 'A') { // Amps - t = LPP_CURRENT; - } else if (*ep == 'm') { - t = LPP_DISTANCE; factor = 0.001f; - } else { - t = 0; - } - - if (t) { - dest[len++] = atoi(parts[i]); // channel number - dest[len++] = t; // LPP type - len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); - } - } - } - return len; -} - /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -269,6 +234,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u // query other sensors -- target specific sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); @@ -322,10 +291,11 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + memcpy(&from->extra.sensor.push_tag, &payload[0], 4); uint16_t timeout_secs; - memcpy(&timeout_secs, &payload[0], 2); - uint8_t reserved = payload[2]; + memcpy(&timeout_secs, &payload[4], 2); + uint8_t reserved = payload[6]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -334,16 +304,17 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u } from->extra.sensor.scope_region_id = r ? r->id : 0; from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = payload[3]; + from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); + memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[8]); + return 8 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success @@ -597,43 +568,6 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char Serial.printf("\n"); } reply[0] = 0; - } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe - uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; - if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - RegionEntry* r; - if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope - r = recv_pkt_region; - } else { // use default scope - r = region_map.getDefaultRegion(); - } - // defaults: - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) - uint16_t timeout_secs = 30*60; // expires after 30 mins - if (command[3] == ' ') { // eg. "sub 300 1:0.2V" - char* cp = &command[4]; - while (*cp >= '0' && *cp <= '9') cp++; - if (cp > &command[4]) { - timeout_secs = atoi(&command[4]); - if (*cp == ' ') { - cp++; // skip the space - from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } - } - } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - if (from->extra.sensor.expiry_timestamp) { - DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); - sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); - } else { - strcpy(reply, "Err - region scope needed"); - } - } else { - strcpy(reply, "Err - no permission"); - } - } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe - from->extra.sensor.scope_region_id = 0; - strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -1172,6 +1106,11 @@ void SensorMesh::loop() { telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // This MCU temperature will be overridden by external sensors (if any) + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { @@ -1184,11 +1123,12 @@ void SensorMesh::loop() { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client - memcpy(reply_data, ×tamp, 4); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + memcpy(reply_data, &c->extra.sensor.push_tag, 4); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(&reply_data[4], ×tamp, 4); + memcpy(&reply_data[8], telemetry.getBuffer(), tlen); - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen); if (reply) { if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, c->out_path, c->out_path_len, 0); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 1e9f472d5..39e71f2c8 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -30,6 +30,7 @@ struct ClientInfo { } room; struct { uint32_t expiry_timestamp; // epoch seconds + uint32_t push_tag; uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From 790680e4d13eb847421d6efc0fe283e0496aaaf5 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:25:34 +1200 Subject: [PATCH 082/127] allow toggling wifi enabled regardless of stored ssid --- examples/companion_radio/MyMesh.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b2c992882..9a5986f9a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2188,14 +2188,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { - uint8_t en = atoi(&command[17]) ? 1 : 0; - if (en && !_prefs.wifiSSID()[0]) { - strcpy(reply, "> set wifi.ssid first"); - return true; - } - _prefs.wifi_enabled = en; + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", en); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); return true; } if (strcmp(command, "get wifi.enabled") == 0) { From bf64fafe9006b1b709c417753773f8ba96cbf714 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:30:00 +1200 Subject: [PATCH 083/127] simplify response message --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9a5986f9a..f745ab2b7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2180,7 +2180,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; savePrefs(); - strcpy(reply, "> wifi config cleared, using build-time credentials (reboot to apply)"); + strcpy(reply, "> wifi config cleared (reboot to apply)"); return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design From 87a05e15896143972690aa8b1bf828fb76c088af Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:33:37 +1200 Subject: [PATCH 084/127] allow remote management of wifi as user has cli permission --- examples/companion_radio/MyMesh.cpp | 90 ++++++++++++++--------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f745ab2b7..4e85e3cb0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2161,54 +2161,50 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } #ifdef WIFI_SSID - // not accepted from the remote mesh CLI (timestamp != 0): these are credentials. The app - // over USB/BLE/WiFi and the serial console (timestamp 0) may set them. - if (sender_timestamp == 0) { - if (memcmp(command, "set wifi.ssid ", 14) == 0) { - StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); - savePrefs(); - sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); - return true; - } - if (memcmp(command, "set wifi.pwd ", 13) == 0) { - StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); - savePrefs(); - strcpy(reply, "> wifi.pwd updated (reboot to apply)"); - return true; - } - if (strcmp(command, "set wifi.clear") == 0) { - _prefs.wifi_ssid[0] = 0; - _prefs.wifi_pwd[0] = 0; - savePrefs(); - strcpy(reply, "> wifi config cleared (reboot to apply)"); - return true; - } - if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); - return true; - } - if (memcmp(command, "set wifi.enabled ", 17) == 0) { - _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; - savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); - return true; - } - if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); - return true; - } - if (strcmp(command, "get wifi.status") == 0) { - strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); - return true; - } - if (strcmp(command, "get wifi.ip") == 0) { - if (WiFi.status() == WL_CONNECTED) { - sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); - } else { - strcpy(reply, "> (not connected)"); - } - return true; + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); + savePrefs(); + sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); + savePrefs(); + strcpy(reply, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "set wifi.clear") == 0) { + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + savePrefs(); + strcpy(reply, "> wifi config cleared (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); + return true; + } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + savePrefs(); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); + return true; + } + if (strcmp(command, "get wifi.status") == 0) { + strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); + } else { + strcpy(reply, "> (not connected)"); } + return true; } #endif From 974f00ded051663b738584fc5c2523dd240b54f3 Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 8 Sep 2026 16:35:31 +1000 Subject: [PATCH 085/127] fix: LR2021 to use correct macro for IRQ_DETECTED in startReceive() --- src/helpers/radiolib/CustomLR2021.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index a89ae9433..0255b93cf 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -72,7 +72,7 @@ class CustomLR2021 : public LR2021 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags - return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } bool isReceiving() { From 8660d1c973d1770275fe36cd990623a5a807b204 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Sep 2026 17:55:02 +1000 Subject: [PATCH 086/127] * refactor: LPP helpers moved to LPPData class --- examples/simple_sensor/SensorMesh.cpp | 126 +++----------------- src/helpers/sensors/LPPDataHelpers.h | 159 ++++++++++++++++++-------- 2 files changed, 128 insertions(+), 157 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 8c6b06a6d..de58e2b16 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,5 +1,6 @@ #include "SensorMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -80,103 +81,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -static uint8_t getDataSize(uint8_t type) { - switch (type) { - case LPP_GPS: - return 9; - case LPP_POLYLINE: - return 8; // TODO: this is MINIMIUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - return 6; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - return 4; - case LPP_COLOUR: - return 3; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - return 2; - } - return 1; -} - -static uint32_t getMultiplier(uint8_t type) { - switch (type) { - case LPP_CURRENT: - case LPP_DISTANCE: - case LPP_ENERGY: - return 1000; - case LPP_VOLTAGE: - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - return 100; - case LPP_TEMPERATURE: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - return 10; - } - return 1; -} - -static bool isSigned(uint8_t type) { - return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || - type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; -} - -static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { - uint32_t value = 0; - for (uint8_t i = 0; i < size; i++) { - value = (value << 8) + buffer[i]; - } - - int sign = 1; - if (is_signed) { - uint32_t bit = 1ul << ((size * 8) - 1); - if ((value & bit) == bit) { - value = (bit << 1) - value; - sign = -1; - } - } - return sign * ((float) value / multiplier); -} - -static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { - // check sign - bool sign = value < 0; - if (sign) value = -value; - - // get value to store - uint32_t v = value * multiplier; - - // format an uint32_t as if it was an int32_t - if (is_signed & sign) { - uint32_t mask = (1 << (size * 8)) - 1; - v = v & mask; - if (sign) v = mask - v + 1; - } - - // add bytes (MSB first) - for (uint8_t i=1; i<=size; i++) { - dest[size - i] = (v & 0xFF); - v >>= 8; - } - return size; -} - static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { uint8_t i = 0; @@ -185,10 +89,10 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); } i += sz; // skip } @@ -209,10 +113,10 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); - float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); if (abs(v - pv) > min_delta) return true; // Yes, has changed @@ -268,12 +172,12 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u auto d = &data[i]; reply_data[ofs++] = d->_channel; reply_data[ofs++] = d->_lpp_type; - uint8_t sz = getDataSize(d->_lpp_type); - uint32_t mult = getMultiplier(d->_lpp_type); - bool is_signed = isSigned(d->_lpp_type); - ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); + uint8_t sz = LPPData::getDataSize(d->_lpp_type); + uint32_t mult = LPPData::getMultiplier(d->_lpp_type); + bool is_signed = LPPData::isSigned(d->_lpp_type); + ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); } return ofs; } @@ -317,9 +221,9 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? - reply_data[4] = 0; // success - getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness - return 8; + memset(&reply_data[4], 0, 8); // success + getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness + return 12 + 2; } return 0; // unknown command } diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 70a036c49..db49cade4 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -63,12 +63,66 @@ #define LPP_ERROR_OVERFLOW 1 #define LPP_ERROR_UNKOWN_TYPE 2 -class LPPReader { - const uint8_t* _buf; - uint8_t _len; - uint8_t _pos; +class LPPData { +public: + static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; + } - float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + return 10; + } + return 1; + } + + static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; + } + + static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { uint32_t value = 0; for (uint8_t i = 0; i < size; i++) { value = (value << 8) + buffer[i]; @@ -85,6 +139,36 @@ class LPPReader { return sign * ((float) value / multiplier); } + static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { + // check sign + bool sign = value < 0; + if (sign) value = -value; + + // get value to store + uint32_t v = value * multiplier; + + // format an uint32_t as if it was an int32_t + if (is_signed & sign) { + uint32_t mask = (1 << (size * 8)) - 1; + v = v & mask; + if (sign) v = mask - v + 1; + } + + // add bytes (MSB first) + for (uint8_t i=1; i<=size; i++) { + dest[size - i] = (v & 0xFF); + v >>= 8; + } + return size; + } + +}; + +class LPPReader { + const uint8_t* _buf; + uint8_t _len; + uint8_t _pos; + public: LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { } @@ -103,72 +187,42 @@ public: } bool readGPS(float& lat, float& lon, float& alt) { - lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3; + lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3; return _pos <= _len; } bool readVoltage(float& voltage) { - voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2; + voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2; return _pos <= _len; } bool readCurrent(float& amps) { - amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; + amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; return _pos <= _len; } bool readPower(float& watts) { - watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2; + watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2; return _pos <= _len; } bool readTemperature(float& degrees_c) { - degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2; + degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2; return _pos <= _len; } bool readPressure(float& pa) { - pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2; + pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2; return _pos <= _len; } bool readRelativeHumidity(float& pct) { - pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1; + pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1; return _pos <= _len; } bool readAltitude(float& m) { - m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2; + m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2; return _pos <= _len; } void skipData(uint8_t type) { - switch (type) { - case LPP_GPS: - _pos += 9; break; - case LPP_POLYLINE: - _pos += 8; break; // TODO: this is MINIMUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - _pos += 6; break; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - _pos += 4; break; - case LPP_COLOUR: - _pos += 3; break; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - _pos += 2; break; - default: - _pos++; - } + _pos += LPPData::getDataSize(type); } }; @@ -185,6 +239,19 @@ class LPPWriter { public: LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { } + bool writeData(uint8_t channel, uint8_t type, float v) { + uint8_t sz = LPPData::getDataSize(type); + bool s = LPPData::isSigned(type); + uint32_t mul = LPPData::getMultiplier(type); + if (_len + 2 + sz <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = type; + _len += LPPData::putFloat(&_buf[_len], v, sz, mul, s); + return true; + } + return false; + } + bool writeVoltage(uint8_t channel, float voltage) { if (_len + 4 <= _max_len) { _buf[_len++] = channel; From 6809fed159444e6f8094b5abc3811ab46db4fdff Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 21:30:17 +1200 Subject: [PATCH 087/127] simplify and remove 2 as a wifi enabled state --- examples/companion_radio/MyMesh.cpp | 4 ++-- examples/companion_radio/NodePrefs.h | 9 +++------ examples/companion_radio/main.cpp | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4e85e3cb0..7a380e982 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2181,7 +2181,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); + sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { @@ -2191,7 +2191,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); + sprintf(reply, "> %d", _prefs.wifi_enabled); return true; } if (strcmp(command, "get wifi.status") == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 85cdebb2d..ed408f9a2 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,12 +47,9 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 2; // 0 = off, 1 = on, 2 = never set (treated as on) - - // effective SSID: stored prefs win over the build-time one - const char* wifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } - // WiFi runs only when there is an SSID and it hasn't been explicitly turned off - bool wifiEnabled() const { return wifiSSID()[0] && wifi_enabled != 0; } + uint8_t wifi_enabled = 1; // enabled by default to allow wifi only builds to work. wifi won't be started if ssid is empty + // use ssid from prefs, or fallback to ssid from build flags + const char* getWifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } #endif private: diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d461a36d0..bfcd26824 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -218,9 +218,9 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - // 'set wifi.enabled 0', or no SSID from either prefs or the build, leaves the radio off entirely - wifi_enabled = the_mesh.getNodePrefs()->wifiEnabled(); - if (wifi_enabled) { + // only start wifi if enabled and ssid is not empty + wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled; + if (wifi_enabled && wifi_ssid[0]) { #if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); From cced091b0c5715d9477d2cde3758e679ad8d4455 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 22:18:22 +1200 Subject: [PATCH 088/127] tidy comments --- examples/companion_radio/main.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index bfcd26824..d886b804f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -209,11 +209,7 @@ void setup() { // add wifi interface #ifdef WIFI_SSID - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). - // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a - // silent fallback to the build-time password of a different network. Copied out of prefs - // so 'set wifi.*' edits only take effect on reboot, as their replies promise. - // (No NULL-for-open-network: the RP2040 core does strlen() on the password unguarded.) + // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults if (the_mesh.getNodePrefs()->wifi_ssid[0]) { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); @@ -239,10 +235,9 @@ void setup() { WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // the join itself blocks inside the core (CYW43::begin busy-waits for the // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the // extra DHCP wait. Give the first connect a full window, then bound the retries below. - // Upgrade path if the stall ever matters: run WiFi on core1. WiFi.beginNoBlock(wifi_ssid, wifi_pwd); last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else From cee43752c7bd8adb44aac5431e9d3350d6ebba17 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 22:54:50 +1200 Subject: [PATCH 089/127] add new ENABLE_WIFI_INTERFACE build flag for companions without providing ssid and pwd --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/NodePrefs.h | 11 +++++++---- examples/companion_radio/main.cpp | 14 ++++++++++---- examples/companion_radio/ui-new/UITask.cpp | 4 ++-- examples/companion_radio/ui-tiny/UITask.cpp | 4 ++-- variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/lilygo_tbeam_1w/platformio.ini | 1 + .../lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/meshnology_w12/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 4 ++-- variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 27 files changed, 51 insertions(+), 17 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7a380e982..2d7d1491a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,7 +1,7 @@ #include "MyMesh.h" #include // needed for PlatformIO -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif #include @@ -2160,7 +2160,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE if (memcmp(command, "set wifi.ssid ", 14) == 0) { StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); savePrefs(); @@ -2438,7 +2438,7 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); -#if defined(WIFI_SSID) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) +#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a // display + long-press), so serve config commands on the otherwise unused USB serial checkCLIRescueCmd(); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ed408f9a2..cf1914d05 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -44,7 +44,10 @@ public: char default_scope_name[31]; uint8_t default_scope_key[16]; int8_t tz_offset = 0; -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; uint8_t wifi_enabled = 1; // enabled by default to allow wifi only builds to work. wifi won't be started if ssid is empty @@ -167,7 +170,7 @@ private: DynamicConfigSerializer custom; -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE class WiFiPrefs : public ConfigSerializer { NodePrefs* _parent; protected: @@ -194,13 +197,13 @@ protected: def("repeat", repeat); def("comp", companion); def("custom", custom); -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE def("wifi", wifi); #endif } public: NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE , wifi(this) #endif { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d886b804f..6dc9acdc3 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,7 +36,13 @@ MultiSerialInterface interface_manager; #endif // include wifi interface -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif + #ifndef WIFI_PWD + #define WIFI_PWD "" + #endif #ifndef TCP_PORT #define TCP_PORT 5000 #endif @@ -121,7 +127,7 @@ void halt() { } /* WIFI RECONNECT TRACKERS */ -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set @@ -208,7 +214,7 @@ void setup() { #endif // add wifi interface -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults if (the_mesh.getNodePrefs()->wifi_ssid[0]) { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); @@ -302,7 +308,7 @@ void loop() { #endif } -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE if (wifi_enabled) { // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 2c79ab916..53ae480cf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -3,7 +3,7 @@ #include "../MyMesh.h" #include "target.h" #include -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -260,7 +260,7 @@ public: sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); display.drawTextCentered(display.width() / 2, 80, tmp); #endif - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index b6bdbcf4b..9ecd66f12 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -4,7 +4,7 @@ #include "target.h" #include "u8g2_icons.h" -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -173,7 +173,7 @@ public: display.setCursor(0, 19); display.print(tmp); - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index df986cf0c..2f07c021b 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -173,6 +173,7 @@ extends = Heltec_RC32 build_flags = ${Heltec_RC32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -315,6 +316,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 178508d88..5fd9edabe 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -182,6 +182,7 @@ extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 28e805543..6435936eb 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -178,6 +178,7 @@ extends = Heltec_lora32_v2 build_flags = ${Heltec_lora32_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 27541a8a5..a2b159ecc 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -185,6 +185,7 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display @@ -341,6 +342,7 @@ lib_deps = extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 25f9ee3bb..96b3eec04 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -228,6 +228,7 @@ extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -394,6 +395,7 @@ extends = heltec_v4_tft build_flags = ${heltec_v4_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 8c2feb946..7611cc1cb 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -175,6 +175,7 @@ extends = heltec_v4_r8_oled build_flags = ${heltec_v4_r8_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -301,6 +302,7 @@ extends = heltec_v4_r8_tft build_flags = ${heltec_v4_r8_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 16db0257d..ab9c9e67c 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -153,6 +153,7 @@ extends = LilyGo_TBeam_1W build_flags = ${LilyGo_TBeam_1W.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index a9991f253..af4a8eafa 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -148,6 +148,7 @@ extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index cf24a5d53..dc60b6f75 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -132,6 +132,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D WIFI_SSID='"ssid"' diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index c0e8c80d9..ffab9d57c 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -171,6 +171,7 @@ extends = meshnology_w12 build_flags = ${meshnology_w12.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 3c5049e06..0f8d31de8 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -143,6 +143,7 @@ extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 9789eabf8..c7e7b0765 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -140,6 +140,7 @@ extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 2fee8c264..5f3b905bd 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -172,6 +172,7 @@ extends = rak3112 build_flags = ${rak3112.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 763c96bc2..9814f3971 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -90,12 +90,11 @@ build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D ENABLE_USB_INTERFACE + -D ENABLE_WIFI_INTERFACE -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH -D BLE_PIN_CODE=123456 ; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) ; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' - -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} @@ -108,6 +107,7 @@ lib_deps = ${rpi_picow.lib_deps} [env:PicoW_companion_radio_wifi] extends = rpi_picow build_flags = ${rpi_picow.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index d8bdd5e81..c1ae765fe 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -224,6 +224,7 @@ extends = Station_G2 build_flags = ${Station_G2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 477c135b6..7006d939e 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -139,6 +139,7 @@ extends = Station_G3_ESP32 build_flags = ${Station_G3_ESP32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index c1f5612a3..862c71543 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -176,6 +176,7 @@ extends = ThinkNode_M2 build_flags = ${ThinkNode_M2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index f64e4e774..a3f348b33 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -190,6 +190,7 @@ extends = ThinkNode_M5 build_flags = ${ThinkNode_M5.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 508fc8139..5606d4822 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -135,6 +135,7 @@ extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=NullDisplayDriver diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 085ab7d51..d4983491d 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -137,6 +137,7 @@ extends = ThinkNode_M9 build_flags = ${ThinkNode_M9.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index cca2907a5..62e6fd216 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -118,6 +118,7 @@ build_src_filter = ${Xiao_esp32_C3.build_src_filter} + build_flags = ${Xiao_esp32_C3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index de656c44d..3bb956070 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -208,6 +208,7 @@ build_src_filter = ${Xiao_S3_WIO.build_src_filter} build_flags = ${Xiao_S3_WIO.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 From 82ea992d869f8ac1a5c1af32a24540978a8d7729 Mon Sep 17 00:00:00 2001 From: Yoshi Walsh Date: Wed, 9 Sep 2026 17:42:35 +1000 Subject: [PATCH 090/127] Fix Git error during project configuration Previously project configuration would fail with "fatal: couldn't find remote ref d541301" (git v2.55.0) Per https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-refspec, the needs to be either a ref or a "**fully spelled** hex object name" This requirement is also mentioned in this SO answer: https://stackoverflow.com/a/30701724 After this change, project configuration now succeeds. 219812 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 2219c9786..de4d6c29c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -85,7 +85,7 @@ platform_packages = ; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 - framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301665b40959682252911e57b11df3ee651a platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} From 0a82fcd20da3f928a7e8e4d0b4516980bead2a7a Mon Sep 17 00:00:00 2001 From: tekk Date: Wed, 9 Sep 2026 22:10:41 +0200 Subject: [PATCH 091/127] fix(companion): gate CMD_SEND_CHANNEL_DATA payload on MAX_GROUP_DATA_LENGTH Payloads of 166 or 167 bytes previously passed the frame-sized bound (MAX_CHANNEL_DATA_LENGTH = 167) but were rejected by sendGroupData against MAX_GROUP_DATA_LENGTH (165), causing ERR_CODE_TABLE_FULL (retry later) to be returned instead of ERR_CODE_ILLEGAL_ARG. Fixes #3345 --- examples/companion_radio/MyMesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 317cf284d..d96e0f76a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1270,8 +1270,8 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } else if (data_type == DATA_TYPE_RESERVED) { writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { - MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { writeOKFrame(); From f722794e50495bd19a5a9f9fb93ed801c445574d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Thu, 10 Sep 2026 09:17:37 +0100 Subject: [PATCH 092/127] Validate set af input to prevent out-of-range airtime factors --- src/helpers/CommonRadioPrefs.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index a25df8806..9d92cad9a 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -67,8 +67,14 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } if (memcmp(command, "set af ", 7) == 0) { - setAirtimeFactor(atof(&command[7])); - strcpy(reply, "OK"); + char* end; + float af = strtof(&command[7], &end); + if (end == &command[7] || af < 0 || af > 9) { + strcpy(reply, "ERROR: af must be 0-9"); + } else { + setAirtimeFactor(af); + strcpy(reply, "OK"); + } return true; } From d69ae98b5a2db3b7484183d5acaac8b3d550f3ff Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 12 Sep 2026 16:53:45 +1000 Subject: [PATCH 093/127] * telemHasChanged() mod, so that only _specified_ min_delta values are considered. --- examples/simple_sensor/SensorMesh.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index de58e2b16..4f2ed8bf1 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -81,7 +81,7 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) { uint8_t i = 0; while (i + 2 < size) { @@ -96,7 +96,7 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u } i += sz; // skip } - return 0.0f; // not found + return def_value; // not found } /* ------------------ end Cayenne LPP helpers ----------------------*/ @@ -117,12 +117,14 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + float min_delta = min_deltas_len > 0 + ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG + : 0.0f; // for ANY change if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes + return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -961,7 +963,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { From 21cdd7b319c0145ebc25dcd05eec34341434a798 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:38:55 -0700 Subject: [PATCH 094/127] Add Muzi Base board support (Duo + Uno + SuperIO) One nRF52840 variant shared across both radios, picked per env: Duo runs the LR1121, Uno the SX1262. Same PCB, only the radio and its DIO1 pin change. SuperIO adds the SH1107 OLED, GPS, buzzer and joystick. GPS is driven by the 3-position mode switch (Mode 2 = on), polled live the way thinknode_m1 does it. Five quick user-button presses power the unit off; a press wakes it back on (arms the button as an nRF52 GPIO SENSE source before SYSTEMOFF). Consolidates #2054 and andyshinn's shared-base/uno work, rebased on dev. Co-authored-by: lbibass Co-authored-by: Andy Shinn --- boards/muzi_base.json | 72 +++++ src/helpers/radiolib/CustomLR1121.h | 41 +++ src/helpers/radiolib/CustomLR1121Wrapper.h | 50 ++++ src/helpers/ui/SH1107Display.cpp | 113 +++++++ src/helpers/ui/SH1107Display.h | 43 +++ variants/muzi_base/muzi_baseBoard.cpp | 52 ++++ variants/muzi_base/muzi_baseBoard.h | 48 +++ variants/muzi_base/platformio.ini | 326 +++++++++++++++++++++ variants/muzi_base/target.cpp | 162 ++++++++++ variants/muzi_base/target.h | 64 ++++ variants/muzi_base/variant.cpp | 90 ++++++ variants/muzi_base/variant.h | 169 +++++++++++ 12 files changed, 1230 insertions(+) create mode 100644 boards/muzi_base.json create mode 100644 src/helpers/radiolib/CustomLR1121.h create mode 100644 src/helpers/radiolib/CustomLR1121Wrapper.h create mode 100644 src/helpers/ui/SH1107Display.cpp create mode 100644 src/helpers/ui/SH1107Display.h create mode 100644 variants/muzi_base/muzi_baseBoard.cpp create mode 100644 variants/muzi_base/muzi_baseBoard.h create mode 100644 variants/muzi_base/platformio.ini create mode 100644 variants/muzi_base/target.cpp create mode 100644 variants/muzi_base/target.h create mode 100644 variants/muzi_base/variant.cpp create mode 100644 variants/muzi_base/variant.h diff --git a/boards/muzi_base.json b/boards/muzi_base.json new file mode 100644 index 000000000..1b2cf7df1 --- /dev/null +++ b/boards/muzi_base.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "muzi_base", + "mcu": "nrf52840", + "variant": "MUZI_BASE", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Muzi Base", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/muzi-works", + "vendor": "MuziWorks" +} diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h new file mode 100644 index 000000000..873bfabc4 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "MeshCore.h" + +class CustomLR1121 : public LR1121 { + bool _rx_boosted = false; + + public: + CustomLR1121(Module *mod) : LR1121(mod) { } + + size_t getPacketLength(bool update) override { + size_t len = LR1121::getPacketLength(update); + if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { + // we've just received a corrupted packet + // this may have triggered a bug causing subsequent packets to be shifted + // call standby() to return radio to known-good state + // recvRaw will call startReceive() to restart rx + MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()"); + standby(); + } + return len; + } + + float getFreqMHz() const { return freqMHz; } + + int16_t setRxBoostedGainMode(bool en) { + _rx_boosted = en; + return LR1121::setRxBoostedGainMode(en); + } + + bool getRxBoostedGainMode() const { return _rx_boosted; } + + bool isReceiving() { + uint16_t irq = getIrqStatus(); + bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); + return detected; + } + uint8_t getSpreadingFactor() const { return spreadingFactor; } + +}; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h new file mode 100644 index 000000000..5361ee238 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CustomLR1121.h" +#include "RadioLibWrappers.h" +#include "LR11x0Reset.h" + +class CustomLR1121Wrapper : public RadioLibWrapper { +public: + CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + + void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + ((CustomLR1121 *)_radio)->setFrequency(freq); + ((CustomLR1121 *)_radio)->setSpreadingFactor(sf); + ((CustomLR1121 *)_radio)->setBandwidth(bw); + ((CustomLR1121 *)_radio)->setCodingRate(cr); + updatePreamble(sf); + } + + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } + bool isReceivingPacket() override { + return ((CustomLR1121 *)_radio)->isReceiving(); + } + float getCurrentRSSI() override { + float rssi = -110; + ((CustomLR1121 *)_radio)->getRssiInst(&rssi); + return rssi; + } + + void onSendFinished() override { + RadioLibWrapper::onSendFinished(); + _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts + } + + uint32_t getEstAirtimeFor(int len_bytes) override { + auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes); + return airtime < 200 ? 200 : airtime; // at least 200 millis + } + + float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); } + + uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); } + + bool setRxBoostedGainMode(bool en) override { + return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; + } + bool getRxBoostedGainMode() const override { + return ((CustomLR1121 *)_radio)->getRxBoostedGainMode(); + } +}; diff --git a/src/helpers/ui/SH1107Display.cpp b/src/helpers/ui/SH1107Display.cpp new file mode 100644 index 000000000..09bf424f8 --- /dev/null +++ b/src/helpers/ui/SH1107Display.cpp @@ -0,0 +1,113 @@ +#include "SH1107Display.h" +#include +#include "Adafruit_SH110X.h" + +#ifndef DISPLAY_ROTATION +#define DISPLAY_ROTATION 0 +#endif + +ColorVal UIColor::window_bkg = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_BLACK; +ColorVal UIColor::title_txt = SH110X_WHITE; +ColorVal UIColor::primary_txt = SH110X_WHITE; +ColorVal UIColor::secondary_txt = SH110X_WHITE; +ColorVal UIColor::warning_txt = SH110X_WHITE; +ColorVal UIColor::popup_bkg = SH110X_BLACK; +ColorVal UIColor::popup_txt = SH110X_WHITE; +ColorVal UIColor::corp_blue = SH110X_WHITE; + +bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr) +{ + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +} + +bool SH1107Display::begin() +{ + bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + if (result) { + display.setRotation(DISPLAY_ROTATION); + } + return result; +} + +void SH1107Display::turnOn() +{ + display.oled_command(SH110X_DISPLAYON); + uint8_t cmd[] = {0xD5, 0xF0}; + display.oled_commandList(cmd, 2); + _isOn = true; +} + +void SH1107Display::turnOff() +{ + display.oled_command(SH110X_DISPLAYOFF); + _isOn = false; +} + +void SH1107Display::clear() +{ + display.clearDisplay(); + display.display(); +} + +void SH1107Display::startFrame(ColorVal bkg) +{ + display.clearDisplay(); // TODO: apply 'bkg' + display.setContrast(120); // 0-127. default setting was causing some flickering. + // display.SH110X_SETPRECHARGE(255); + _color = SH110X_WHITE; + display.setTextColor(_color); + display.setTextSize(1); + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void SH1107Display::setTextSize(int sz) +{ + display.setTextSize(sz); +} + +void SH1107Display::setColor(ColorVal c) +{ + _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + display.setTextColor(_color); +} + +void SH1107Display::setCursor(int x, int y) +{ + display.setCursor(x, y); +} + +void SH1107Display::print(const char *str) +{ + display.print(str); +} + +void SH1107Display::fillRect(int x, int y, int w, int h) +{ + display.fillRect(x, y, w, h, _color); +} + +void SH1107Display::drawRect(int x, int y, int w, int h) +{ + display.drawRect(x, y, w, h, _color); +} + +void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) +{ + display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); +} + +uint16_t SH1107Display::getTextWidth(const char *str) +{ + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + +void SH1107Display::endFrame() +{ + display.display(); +} diff --git a/src/helpers/ui/SH1107Display.h b/src/helpers/ui/SH1107Display.h new file mode 100644 index 000000000..417184c29 --- /dev/null +++ b/src/helpers/ui/SH1107Display.h @@ -0,0 +1,43 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#define SH110X_NO_SPLASH +#include + +#ifndef PIN_OLED_RESET +#define PIN_OLED_RESET -1 +#endif + +#ifndef DISPLAY_ADDRESS +#define DISPLAY_ADDRESS 0x3c +#endif + +class SH1107Display : public DisplayDriver +{ + Adafruit_SH1107 display; + bool _isOn; + uint8_t _color; + + bool i2c_probe(TwoWire &wire, uint8_t addr); + +public: + SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; } + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; + void setTextSize(int sz) override; + void setColor(ColorVal c) override; + void setCursor(int x, int y) override; + void print(const char *str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; + uint16_t getTextWidth(const char *str) override; + void endFrame() override; +}; diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/muzi_baseBoard.cpp new file mode 100644 index 000000000..ed713fa07 --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include "muzi_baseBoard.h" + +#ifdef NRF52_POWER_MANAGEMENT +const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +void muzi_baseBoard::initiateShutdown(uint8_t reason) { + // Disable LoRa module power before shutdown + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + enterSystemOff(reason); +} +#endif // NRF52_POWER_MANAGEMENT + +void muzi_baseBoard::begin() { + NRF52BoardDCDC::begin(); + pinMode(PIN_VBAT_READ, INPUT); +#ifdef muzi_base_superIO + // 12V rail is only needed for the superIO display + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display + delay(250); +#endif + Wire.begin(); + // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. + +#ifdef PIN_USER_BTN + pinMode(PIN_USER_BTN, INPUT_PULLUP); +#endif + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + +// #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) +// Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +// #endif +#ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); +#endif +} diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/muzi_baseBoard.h new file mode 100644 index 000000000..e5412b6a8 --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +// The Muzi Base PCB ships in two radio flavors. Keep the user-facing identity +// (OTA/DFU name and reported manufacturer) distinct per radio even though the +// board logic is shared. +#if defined(USE_SX1262) + #define MUZI_BASE_OTA_NAME "MUZI_BASE_UNO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Uno" +#else + #define MUZI_BASE_OTA_NAME "MUZI_BASE_DUO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Duo" +#endif + +class muzi_baseBoard : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + void begin(); + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(1); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + // ADC_MULTIPLIER is the voltage divider ratio + return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return MUZI_BASE_MFR_NAME; + } +}; diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini new file mode 100644 index 000000000..eb35d5696 --- /dev/null +++ b/variants/muzi_base/platformio.ini @@ -0,0 +1,326 @@ +; ============================================================================ +; Muzi Base (MuziWorks) +; +; One nRF52840 board, shipped in two radio flavors. The radio is selected per +; environment; all board logic lives in variants/muzi_base and is shared: +; muzi_base_duo_* -> LR1121 (USE_LR1121) +; muzi_base_uno_* -> SX1262 (USE_SX1262) +; ============================================================================ + +[muzi_base_common] +extends = nrf52_base +board = muzi_base +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/muzi_base + -I src/helpers/ui + -D muzi_base + -D NRF52_POWER_MANAGEMENT + -D PIN_USER_BTN=PIN_BUTTON1 + -D USER_BTN_PRESSED=LOW + -D PIN_STATUS_LED=35 + -D LORA_TX_POWER=22 + -D P_LORA_BUSY=LORA_BUSY + -D P_LORA_SCLK=LORA_SCLK + -D P_LORA_NSS=LORA_NSS + -D P_LORA_DIO_1=LORA_DIO_1 + -D P_LORA_MISO=LORA_MISO + -D P_LORA_MOSI=LORA_MOSI + -D P_LORA_RESET=LORA_RESET + -D QSPIFLASH=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/muzi_base> +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = + ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + +; --------------------------------------------------------------------------- +; Radio selection (Duo = LR1121, Uno = SX1262). The matching radio pins / TCXO +; / RF-switch defines are picked up in variant.h via USE_LR1121 / USE_SX1262. +; --------------------------------------------------------------------------- +[muzi_base_duo] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_LR1121 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=3.0 + +[muzi_base_uno] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + +; =========================================================================== +; Duo (LR1121) environments +; =========================================================================== +[env:muzi_base_duo_repeater] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_room_server] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_companion_radio_usb] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_duo.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_duo_companion_radio_ble] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_duo_superIO] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_duo_companion_radio_ble_superIO] +extends = muzi_base_duo_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_duo_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_duo_superIO.lib_deps} + +; =========================================================================== +; Uno (SX1262) environments +; =========================================================================== +[env:muzi_base_uno_repeater] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_room_server] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_companion_radio_usb] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_uno.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_uno_companion_radio_ble] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_uno_superIO] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_uno_companion_radio_ble_superIO] +extends = muzi_base_uno_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_uno_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_uno_superIO.lib_deps} diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp new file mode 100644 index 000000000..2af0c7107 --- /dev/null +++ b/variants/muzi_base/target.cpp @@ -0,0 +1,162 @@ +#include +#include +#include "target.h" +#include "variant.h" + +muzi_baseBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + MuziBaseSensorManager sensors = MuziBaseSensorManager(nmea); +#else + MuziBaseSensorManager sensors; +#endif + +// user button for the N-click power-off. single-click mode (multiclick off) so +// each press is its own CLICK; we count them ourselves. reliable across the +// idle-sleep loop because it's the same MomentaryButton the UI navigates with. +static MomentaryButton pwr_btn(PIN_USER_BTN, 0, true, true, false); + +// power off, arming the user button as a wake source first so a later press +// turns it back on. wait for the button to release (the 5th click already fires +// on release, so this returns quickly), then set nRF52 GPIO SENSE (press = LOW); +// it survives into SYSTEMOFF because shutdownPeripherals() doesn't touch this pin. +static void muziPowerOff() { + uint32_t t0 = millis(); + while (digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED && (millis() - t0) < 3000) { delay(5); } + delay(50); // settle + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + board.powerOff(); // shutdownPeripherals() + SYSTEMOFF +} + +bool MuziBaseSensorManager::begin() { + pwr_btn.begin(); + bool ok = EnvironmentSensorManager::begin(); +#if ENV_INCLUDE_GPS + pinMode(PIN_GPS_SWITCH, INPUT); + _last_gps_sw = digitalRead(PIN_GPS_SWITCH); // initial gps state from the switch + if (_last_gps_sw == HIGH) start_gps(); else stop_gps(); +#endif + return ok; +} + +void MuziBaseSensorManager::loop() { + // user button: USER_BTN_POWER_OFF_CLICKS quick clicks -> power off + if (pwr_btn.check() == BUTTON_EVENT_CLICK) { + unsigned long t = millis(); + if (t - _pwr_last_click > 2000) _pwr_clicks = 0; // restart if too slow + _pwr_last_click = t; + if (++_pwr_clicks >= USER_BTN_POWER_OFF_CLICKS) muziPowerOff(); + } +#if ENV_INCLUDE_GPS + unsigned long now = millis(); + if (now > _next_sw_check) { // check the mode switch ~once a sec + _next_sw_check = now + 1000; + int sw = digitalRead(PIN_GPS_SWITCH); + if (sw != _last_gps_sw) { + _last_gps_sw = sw; + if (sw == HIGH) start_gps(); else stop_gps(); + } + } +#endif + EnvironmentSensorManager::loop(); +} + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +#if defined(USE_LR1121) + #ifndef LORA_CR + #define LORA_CR 5 + #endif + + #ifdef RF_SWITCH_TABLE + static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC + }; + + static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, + }; + #endif +#endif // USE_LR1121 + +bool radio_init() { + //rtc_clock.begin(Wire); + +#if defined(USE_LR1121) + #ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; + #else + float tcxo = 1.6f; + #endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + + #ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); + #endif + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + return true; // success +#else // USE_SX1262 + // CustomSX1262::std_init() configures the SPI pins, runs begin() with the + // TCXO-voltage fallback, sets CRC, current limit, the DIO2 RF switch, and + // RX boosted gain from the SX126X_* build flags. + return radio.std_init(&SPI); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h new file mode 100644 index 000000000..cbcfff141 --- /dev/null +++ b/variants/muzi_base/target.h @@ -0,0 +1,64 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "muzi_baseBoard.h" +#if defined(USE_LR1121) + #include +#elif defined(USE_SX1262) + #include +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif +#include +#include +#include +#include +#include // Added: Include for EnvironmentSensorManager +#include + + +#ifdef muzi_base_superIO + #include + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#elif defined(DISPLAY_CLASS) + #include "helpers/ui/NullDisplayDriver.h" + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern muzi_baseBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +// muzi sensor manager, polled from main loop via sensors.loop(): +// - user button: USER_BTN_POWER_OFF_CLICKS quick presses -> power off (all builds) +// - gps mode switch on/off (superIO only, see thinknode_m1) +class MuziBaseSensorManager : public EnvironmentSensorManager { + unsigned long _pwr_last_click = 0; + uint8_t _pwr_clicks = 0; +#if ENV_INCLUDE_GPS + int _last_gps_sw = -1; + unsigned long _next_sw_check = 0; +#endif +public: +#if ENV_INCLUDE_GPS + MuziBaseSensorManager(LocationProvider& location) : EnvironmentSensorManager(location) {} +#else + MuziBaseSensorManager() {} +#endif + bool begin() override; + void loop() override; +}; +extern MuziBaseSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp new file mode 100644 index 000000000..b179af8ac --- /dev/null +++ b/variants/muzi_base/variant.cpp @@ -0,0 +1,90 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, +// P1 pins. + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, +}; + +void initVariant() +{ + // All pins output HIGH by default. + // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 + + pinMode(PIN_VBAT_READ, INPUT); + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(PIN_BUTTON1, INPUT); + pinMode(PIN_BUTTON2, INPUT); + pinMode(PIN_BUTTON3, INPUT); + pinMode(PIN_BUTTON4, INPUT); + pinMode(PIN_BUTTON5, INPUT); + pinMode(PIN_BUTTON6, INPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_PIN, LOW); + digitalWrite(LED_BLUE, LOW); + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. +} diff --git a/variants/muzi_base/variant.h b/variants/muzi_base/variant.h new file mode 100644 index 000000000..b007c1f8b --- /dev/null +++ b/variants/muzi_base/variant.h @@ -0,0 +1,169 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_VBAT_READ (31) // P0.31 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.537 +#define ADC_RESOLUTION 14 +#define PIN_BATTERY_CHARGING (32+2) // P1.02 STAT2 +#define PIN_CHARGER_FAULT (27) // P0.27 STAT1 this pin is disabled on meshtastic. +// BQ25185 has 2 status pins: STAT1 and STAT2. Both are high when not charging. STAT1 high, STAT2 low: charging. Recoverable fault: STAT1 low, STAT2 high. Unrecoverable fault: both low. +// We only need to detect charging vs not charging, but someone else can use the fault pin to log when the battery gets too hot or cold. + +// Power management boot protection threshold (millivolts) +#define PWRMGT_VOLTAGE_BOOTLOCK 3100 // Won't boot below this voltage (mV). BB15 battery min voltage is 3v, 3100mV is minimum batt in meshtastic code. + +// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) +#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT_READ +#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V) was the default on RAK4631. should still apply here. + +// Other pins +#define PIN_AREF (-1) +#define SCREEN_12V_ENABLE (23) // SH1107 OLED controller has a pin that needs to be enabled to turn on the screen. + +static const uint8_t AREF = (PIN_AREF); // not used + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (19) // P0.19 used for GPS RX +#define PIN_SERIAL1_TX (20) // P0.20 used for GPS TX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE1_SDA (4) // P0.4 +#define PIN_WIRE1_SCL (6) // P0.6 +#define PIN_WIRE_SDA (24) // P0.24 OLED I2C +#define PIN_WIRE_SCL (25) // P0.25 OLED I2C +#define I2C_NO_RESCAN +// #define I2C_NO_RESCAN +// #define HAS_QMA6100P +// #define QMA_6100P_INT_PIN (34) // P1.2 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (32+15) // internally connected to p1.15 +#define PIN_SPI_MOSI (32+14) // internally connected to p1.14 +#define PIN_SPI_SCK (32+13) // internally connected to p1.13 +#define PIN_SPI_NSS (32+12) // internally connected to p1.12 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (35) +#define LED_BLUE (-1) // P1.04 turned off, because the blue LED was annoying. +// #define LED_GREEN (35) // P1.03 +#define LED_PIN LED_BUILTIN + +#define LED_STATE_ON LOW + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons +#define PIN_BUTTON1 (10) // P0.10 Menu / User Button | on superIO, this is in the center of the "D-Pad", but it's also the button on the Uno/Duo. +#define PIN_BUTTON2 (21) // Joystick Up +#define PIN_BUTTON3 (17) // Joystick Down +#define PIN_BUTTON4 (37) // Joystick Left +#define PIN_BUTTON5 (16) // Joystick Right +#define PIN_BUTTON6 (15) // Back / Cancel Button. +#define JOYSTICK_PRESS PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define PIN_BACK_BTN PIN_BUTTON6 + +// quick user-button presses that power the device off (per Muzi's docs) +#ifndef USER_BTN_POWER_OFF_CLICKS +#define USER_BTN_POWER_OFF_CLICKS 5 +#endif + +//////////////////////////////////////////////////////////////////////////////// +// LoRa radio +// +// The Muzi Base PCB is populated with one of two radios: +// * Base Uno -> SX1262 (USE_SX1262) +// * Base Duo -> LR1121 (USE_LR1121) +// Both share NSS/SCLK/MISO/MOSI/RESET/BUSY. The only pin difference is the +// radio IRQ (DIO1): the SX1262 routes it to P1.06, the LR1121 to P1.08. +// (see Meshtastic muzi_base variant, which defines both radios on this board) + +#define LORA_NSS (PIN_SPI_NSS) // P1.12 +#define LORA_RESET (32+10) // P1.10 +#define LORA_BUSY (32+11) // P1.11 +#define LORA_SCLK (PIN_SPI_SCK) // P1.13 +#define LORA_MISO (PIN_SPI_MISO) // P1.15 +#define LORA_MOSI (PIN_SPI_MOSI) // P1.14 + +// only the IRQ pin differs per radio. rf-switch/tcxo defines are in platformio.ini +#if defined(USE_LR1121) + #define LORA_DIO_1 (32+8) // P1.08 LR1121 IRQ/DIO1 +#elif defined(USE_SX1262) + #define LORA_DIO_1 (32+6) // P1.06 SX1262 IRQ/DIO1 +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif + +//////////////////////////////////////////////////////////////////////////////// +// QSPI Flash +#define PIN_QSPI_SCK (0 + 3) +#define PIN_QSPI_CS (0 + 26) +#define PIN_QSPI_IO0 (0 + 30) +#define PIN_QSPI_IO1 (0 + 29) +#define PIN_QSPI_IO2 (0 + 28) +#define PIN_QSPI_IO3 (0 + 2) + +#define EXTERNAL_FLASH_DEVICES W25Q128JVPQ +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// GPS +#define HAS_GPS 1 +#define PIN_GPS_RX PIN_SERIAL1_RX +#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_EN (32+1) // P1.01 PWR_IO2 on schematic. gps power, driven by the sensor manager. + +// superIO 3-position mode switch (pins from the meshtastic muzi_base variant). +// "Mode 2" reads HIGH on PIN_GPS_SWITCH and turns the gps on. +#define SWITCH_MODE1 (32+9) // P1.09 +#define SWITCH_MODE2 (12) // P0.12 +#define PIN_GPS_SWITCH SWITCH_MODE2 + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_PIN (22) // P0.22 same load switch design as GPS_EN. From df82dc23390097e72fdef829d48a34b124cf1036 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:05:38 +1000 Subject: [PATCH 095/127] * min_deltas now mandatory --- examples/simple_sensor/SensorMesh.cpp | 49 ++++++++++++++------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 4f2ed8bf1..54bce50dd 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -102,29 +102,24 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { - if (telemetry.getSize() != prev_telem_size) return true; - auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); uint8_t i = 0; - while (i + 2 < size) { - // Get channel # - uint8_t ch = buf[i++]; - // Get data type - uint8_t t = buf[i++]; + while (i + 2 < min_deltas_len) { + uint8_t ch = min_deltas[i++]; // Get channel # + uint8_t t = min_deltas[i++]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = min_deltas_len > 0 - ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG - : 0.0f; // for ANY change + float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + float v = findTelemValue(buf, size, ch, t, 0.0f); + float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) + return false; // none of the -specified- telemetry values changed by min_delta } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -202,26 +197,34 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u uint16_t timeout_secs; memcpy(&timeout_secs, &payload[4], 2); uint8_t reserved = payload[6]; + uint8_t min_deltas_len = payload[7]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; } else { // use default scope r = region_map.getDefaultRegion(); } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); - // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); - - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) - strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 8 + strlen((char *)&reply_data[8]); + uint8_t reply_len; + if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) { + from->extra.sensor.scope_region_id = r->id; + from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; + from->extra.sensor.min_deltas_len = min_deltas_len; + memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); + // reply with actual expiry timestamp + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + reply_len = 8 + strlen((char *)&reply_data[8]); + } else { + memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + reply_len = 8; + } + return reply_len; } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; + from->extra.sensor.min_deltas_len = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? memset(&reply_data[4], 0, 8); // success getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness @@ -1021,7 +1024,7 @@ void SensorMesh::loop() { // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { auto c = acl.getClientByIdx(i); - if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired From c6b051d063c611f892fba0341919a969e36991a9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:40:25 +1000 Subject: [PATCH 096/127] * request payload doco --- docs/payloads.md | 52 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/payloads.md b/docs/payloads.md index fb9cbaf99..493f628ce 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | request sub type | | request data | rest of payload | application-defined request payload body | For the common chat/server helpers in `BaseChatMesh`, the current request type values are: @@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | Value | Name | Description | |--------|-----------|----------------------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | keep-alive request used for maintained connections | +| `0x02` | keepalive | (deprecated) | +| `0x03` | get telemetry | request node telemetry | +| `0x04` | get min/max/avg | get sensor node stats on time series data | +| `0x05` | get acl | node ACL query | +| `0x06` | get neighbors | node neighbors query | +| `0x08` | subscribe | subscribe to telemetry push | +| `0x09` | ubsubscribe | unsubscribe from telemetry push | #### Get stats @@ -125,21 +132,50 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -#### Get telemetry data - -Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. - #### Get Telemetry -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x03 (request sub type) | +| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) | #### Get Min/Max/Ave (Sensor nodes) -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x04 (request sub type) | +| start | 4 | starting time, seconds ago | +| end | 4 | ending time, seconds ago | +| reserved | 2 | should be zeroes | + +#### Subscribe to Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x08 (request sub type) | +| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads | +| timeout secs | 2 | subscription timeout (seconds) | +| reserved | 1 | should be zero | +| min deltas len | 1 | byte length of LPP encoded min_deltas | +| min deltas | (variable) | LPP encoded min_deltas | + +#### Unsubscribe from Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x09 (request sub type) | #### Get Access List -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x05 (request sub type) | +| reserved | 2 | should be zeroes | #### Get Neighbors From bc8ccefafe1423ba50a0932da815a40c063e6121 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Sep 2026 20:23:10 +1000 Subject: [PATCH 097/127] * fix for telemHasChanged() --- examples/simple_sensor/SensorMesh.cpp | 41 ++++++++++++++++++--------- examples/simple_sensor/SensorMesh.h | 4 +-- src/helpers/ClientACL.h | 1 + 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 54bce50dd..70a619f7f 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -101,25 +101,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ -bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { +bool SensorMesh::telemHasChanged(ClientInfo* c) { auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); uint8_t i = 0; + bool changed = false; - while (i + 2 < min_deltas_len) { - uint8_t ch = min_deltas[i++]; // Get channel # - uint8_t t = min_deltas[i++]; // Get data type + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float v = findTelemValue(buf, size, ch, t, 0.0f); - float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); - if (abs(v - pv) > min_delta) return true; // Yes, has changed + if (abs(v - pv) > min_delta) changed = true; // Yes, has changed - i += sz; // skip + i += 2 + sz; // skip } - return false; // none of the -specified- telemetry values changed by min_delta + if (changed) { + // take snapshot of all _monitored_ telem values, for next cycle + i = 0; + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type + uint8_t sz = LPPData::getDataSize(t); + + c->extra.sensor.prev_telem[i] = ch; + c->extra.sensor.prev_telem[i + 1] = t; + + float v = findTelemValue(buf, size, ch, t, 0.0f); + LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + i += 2 + sz; // skip + } + } + return changed; } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -774,7 +792,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; - prev_telem_size = 0; region_load_active = false; // defaults @@ -1028,7 +1045,7 @@ void SensorMesh::loop() { RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired - if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + if (telemHasChanged(c)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); @@ -1048,8 +1065,6 @@ void SensorMesh::loop() { } } } - memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle - prev_telem_size = telemetry.getSize(); onSensorDataRead(); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e0e03c81b..6b7a32fef 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -148,8 +148,6 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; - uint8_t prev_telem_size; - uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; @@ -166,7 +164,7 @@ private: uint8_t pending_cr; bool region_load_active; - bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); + bool telemHasChanged(ClientInfo* c); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 39e71f2c8..9938bc1e1 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,6 +34,7 @@ struct ClientInfo { uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded + uint8_t prev_telem[14]; // LPP encoded } sensor; } extra; From 3819a0bd897b63e1ec8e11d0bb34d83a107ffb00 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Sep 2026 17:17:48 +1000 Subject: [PATCH 098/127] * subscribe response now returns expiry_secs (not timestamp) --- examples/simple_sensor/SensorMesh.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 70a619f7f..b18606681 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -228,12 +228,13 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; from->extra.sensor.min_deltas_len = min_deltas_len; memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); - // reply with actual expiry timestamp - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + // reply with actual expiry seconds (we could modify/impose restriction) + memcpy(&reply_data[4], &timeout_secs, 2); + memset(&reply_data[6], 0, 2); // reserved strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used reply_len = 8 + strlen((char *)&reply_data[8]); } else { - memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + memset(&reply_data[4], 0, 4); // expiry secs (0 for error) reply_len = 8; } return reply_len; From 3a3c13a143c9120bae648d1b5ddb8e12483e8695 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Sep 2026 18:41:11 +1000 Subject: [PATCH 099/127] * refactor to support new Board::handleCommand(), and "get/set radio.fem.*" commands --- .../wio-tracker-l1-1w/WioTrackerL1Board.cpp | 51 ++++++++++++++++++- .../wio-tracker-l1-1w/WioTrackerL1Board.h | 10 ++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp index 8210c24ed..c6dbdbf58 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -15,7 +15,6 @@ void WioTrackerL1Board::begin() { pinMode(PIN_BUTTON4, INPUT_PULLUP); pinMode(PIN_BUTTON5, INPUT_PULLUP); pinMode(PIN_BUTTON6, INPUT_PULLUP); - #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); @@ -32,9 +31,57 @@ void WioTrackerL1Board::begin() { delay(10); // give sx1262 some time to power up } - + bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { _is_pa_enabled = enable; digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA return true; } + +void WioTrackerL1Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool WioTrackerL1Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + strcpy(reply, "OK - LoRa FEM TX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index c5a609443..798145db0 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -8,10 +8,16 @@ class WioTrackerL1Board : public NRF52BoardDCDC { protected: uint8_t btn_prev_state; bool _is_pa_enabled = false; + KeyValueStore* _prefs = NULL; + + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const { return _is_pa_enabled; } public: WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { @@ -35,10 +41,6 @@ public: return "Seeed Wio Tracker L1"; } - bool canControlLoRaFemPaGain() const override { return true; } - bool setLoRaFemPaGainEnabled(bool enable) override; - bool isLoRaFemPaGainEnabled() const override { return _is_pa_enabled; } - void powerOff() override { setLoRaFemPaGainEnabled(false); // turn Off PA NRF52Board::powerOff(); From 9d6d53a71a0f1a84cb337bdf6bff1d2497347feb Mon Sep 17 00:00:00 2001 From: Und3r_1337 Date: Wed, 16 Sep 2026 00:03:54 +0200 Subject: [PATCH 100/127] fix(config): allow digits in config keys so keys like gr_1hop round-trip --- src/helpers/ConfigSerializer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 36aff5ccf..daf834b2b 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -24,7 +24,9 @@ static bool is_whitespace(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static bool is_key_char(char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; + // digits are allowed: keys like gr_1hop contain one, otherwise loadSerial + // fails at that key and prefs stop reloading after a reboot + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } static bool is_value_char(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; From 3caf033de2fad1ce9c0b6f5d957cacfdb4460d26 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 16 Sep 2026 21:03:44 +1000 Subject: [PATCH 101/127] * refactor: removed UITask dependency from MyMesh. New MyMesh::Listener interface * UITask's are now a MyMesh task Listener --- examples/companion_radio/AbstractUITask.h | 11 ++--- examples/companion_radio/MyMesh.cpp | 48 +++++++-------------- examples/companion_radio/MyMesh.h | 14 ++++-- examples/companion_radio/main.cpp | 7 +-- examples/companion_radio/ui-new/UITask.cpp | 48 ++++++++++++++++----- examples/companion_radio/ui-new/UITask.h | 7 ++- examples/companion_radio/ui-orig/UITask.cpp | 47 +++++++++++++++++--- examples/companion_radio/ui-orig/UITask.h | 8 +++- examples/companion_radio/ui-tiny/UITask.cpp | 30 +++++++++++-- examples/companion_radio/ui-tiny/UITask.h | 7 ++- 10 files changed, 152 insertions(+), 75 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index b25b1442f..d927c1a3f 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -12,6 +12,7 @@ #endif #include "NodePrefs.h" +#include "MyMesh.h" enum class UIEventType { none, @@ -22,25 +23,21 @@ enum class UIEventType { ack }; -class AbstractUITask { +class AbstractUITask : public MyMesh::Listener { protected: mesh::MainBoard* _board; MultiSerialInterface* _interfaceManager; - bool _connected; AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { - _connected = false; } public: - void setHasConnection(bool connected) { _connected = connected; } - bool hasConnection() const { return _connected; } + bool hasConnection() const { return _interfaceManager->isConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); } void disableBluetooth() { _interfaceManager->disableBluetooth(); } - virtual void msgRead(int msgcount) = 0; - virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; + virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01a7473a0..7becb0dfc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -368,10 +368,6 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); } - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::newContactMessage); -#endif } // add inbound-path to mem cache @@ -395,6 +391,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path p->path_len = mesh::Packet::copyPath(p->path, path, path_len); } + if (_listener) _listener->onDiscoveredContact(contact, is_new, path_len, path); + if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] } @@ -520,16 +518,12 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS // we only want to show text messages on display, not cli data bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _ui) { - _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_serial->isConnected()) { - _ui->notify(UIEventType::contactMessage); - } + if (should_display && _listener) { + _listener->onMessageRecv(path_len, from.name, text); + _listener->onQueueSizeChanged(offline_queue_len); } -#endif } bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { @@ -640,20 +634,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::channelMessage); -#endif } -#ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; + if (_listener) { + // Get the channel name from the channel index + ChannelDetails channel_details; + if (!getChannel(channel_idx, channel_details)) { + strcpy(channel_details.name, "Unknown"); + } + _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onQueueSizeChanged(offline_queue_len); } - if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len); -#endif } void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, @@ -930,9 +920,9 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} -MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) { _iter_started = false; _cli_rescue = false; cli_command[0] = 0; @@ -1463,9 +1453,7 @@ void MyMesh::handleCmdFrame(size_t len) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); -#endif + if (_listener) _listener->onQueueSizeChanged(offline_queue_len); } else { out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; _serial->writeFrame(out_frame, 1); @@ -2466,10 +2454,6 @@ void MyMesh::loop() { saveContacts(); dirty_contacts_expiry = 0; } - -#ifdef DISPLAY_CLASS - if (_ui) _ui->setHasConnection(_serial->isConnected()); -#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3b98a4f67..be7bc40a5 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -2,7 +2,6 @@ #include #include -#include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ #define FIRMWARE_VER_CODE 14 @@ -96,10 +95,19 @@ struct DiscoveredNode { class MyMesh : public BaseChatMesh, public DataStoreHost { public: - MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); + class Listener { + public: + virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onQueueSizeChanged(int msgcount) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + }; + + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); void begin(bool has_display); void startInterface(BaseSerialInterface &serial); + void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); @@ -237,7 +245,7 @@ private: uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; - AbstractUITask* _ui; + Listener* _listener; ContactsIterator _iter; uint32_t _iter_filter_since; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc3..857acf451 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -114,11 +114,7 @@ MultiSerialInterface interface_manager; StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif -); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); /* END GLOBAL OBJECTS */ @@ -285,6 +281,7 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved + the_mesh.setListener(&ui_task); #endif board.onBootComplete(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 53ae480cf..fc6707e29 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,17 +751,7 @@ switch(t){ #endif } - -void UITask::msgRead(int msgcount) { - _msgcount = msgcount; - if (msgcount == 0) { - gotoHomeScreen(); - } -} - -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); @@ -774,6 +764,42 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); + setCurrScreen(msg_preview); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onQueueSizeChanged(int msgcount) { + _msgcount = msgcount; + if (msgcount == 0) { + gotoHomeScreen(); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52d3ffa13..e12009538 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -90,10 +90,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b49b05dd1..08c845874 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -126,7 +126,7 @@ switch(t){ // Serial.println((int) t); } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { clearMsgPreview(); @@ -139,9 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -158,10 +156,45 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _display->turnOn(); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _need_refresh = true; + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +#ifdef HAS_DRV2605 + vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) +#endif + + if (path_len == 0xFF) { + sprintf(_origin, "(F) %s", channel_details.name); + } else { + sprintf(_origin, "(%d) %s", (uint32_t) path_len, channel_details.name); + } + StrHelper::strncpy(_msg, text, sizeof(_msg)); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { @@ -262,7 +295,7 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && the_mesh.getBLEPin() != 0) { + if (!hasConnection() && the_mesh.getBLEPin() != 0) { _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 02d126e8f..381414791 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -70,9 +70,13 @@ public: bool hasDisplay() const { return _display != NULL; } void clearMsgPreview(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 9ecd66f12..8a8b77b53 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -503,16 +503,14 @@ switch(t){ } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { gotoHomeScreen(); } } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -522,6 +520,30 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index dc689478e..925e8d719 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -98,10 +98,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; From b71933beb5d618aa74f99c4916bda659bb6e22a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Wed, 16 Sep 2026 22:28:55 +0200 Subject: [PATCH 102/127] Enable DC/DC in shared LR1110 initialization --- src/helpers/radiolib/CustomLR1110.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1df..f0fde4354 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1110::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { From 5c3d9281ffbd5c1cb8ee5570c0d89aebbac91521 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 13:45:33 +1000 Subject: [PATCH 103/127] * refactored the UI_DISCOVER_* feature --- examples/companion_radio/MyMesh.cpp | 64 ++---------------- examples/companion_radio/MyMesh.h | 35 ++-------- examples/companion_radio/ui-new/UITask.cpp | 72 +++++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 23 ++++++- examples/companion_radio/ui-orig/UITask.cpp | 12 +++- examples/companion_radio/ui-orig/UITask.h | 3 +- examples/companion_radio/ui-tiny/UITask.cpp | 8 ++- examples/companion_radio/ui-tiny/UITask.h | 3 +- 8 files changed, 112 insertions(+), 108 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7becb0dfc..6e570ea50 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -139,10 +139,6 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 -// Copied from simple_repeater (could probably be shared) -#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 -#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 - #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -410,53 +406,6 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { - if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; - if (max_num > disc_nodes_count) max_num = disc_nodes_count; - - for (int i = 0; i < max_num; i++) { - nodes[i] = discovered_nodes[i]; - } - return max_num; -} - -bool MyMesh::requestRepeatersDiscovery() { - uint8_t cmd_bytes[6]; - cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only - cmd_bytes[1] = 0xFF; // Repeaters - getRNG()->random(&cmd_bytes[2], 4); // tag - disc_nodes_count = 0; - disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); - mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); - if (req) { - sendZeroHop(req); - return true; - } - return false; -} - -void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { - if ((p_len < 12) - || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) - || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) - || (memcmp(&payload[2], &disc_node_req_tag, 4))) { - return; - } - memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); - discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; - discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; - discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); - ContactInfo* c = lookupContactByPubKey(&payload[6], 8); - if (c != NULL) { - strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); - } else { - discovered_nodes[disc_nodes_count].name[0] = 0; - } - disc_nodes_count ++; -} -#endif - void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -518,10 +467,8 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _listener) { - _listener->onMessageRecv(path_len, from.name, text); + if (_listener) { + _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -840,9 +787,6 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); -#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); @@ -856,6 +800,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); } + + if (_listener) _listener->onControlDataRecv(packet); } void MyMesh::onRawDataRecv(mesh::Packet *packet) { @@ -1039,7 +985,7 @@ void MyMesh::begin(bool has_display) { resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure public channel _store->loadChannels(this); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index be7bc40a5..f11286249 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -75,6 +75,10 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +// Copied from simple_repeater +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + struct AdvertPath { uint8_t pubkey_prefix[7]; uint8_t path_len; @@ -83,24 +87,15 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -struct DiscoveredNode { - uint8_t pubkey_prefix[9]; - float snr_in; - float snr_out; - char name[32]; - uint8_t type; -}; -#endif - class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + virtual void onControlDataRecv(const mesh::Packet* packet) = 0; }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); @@ -120,11 +115,6 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - bool requestRepeatersDiscovery(); - int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); -#endif - protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -287,19 +277,6 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - #ifdef UI_RECENT_LIST_SIZE - #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE - #else - #define DISCOVERED_NODES_TABLE_SIZE 4 - #endif - DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept - uint32_t disc_node_req_tag = 0; - uint32_t disc_nodes_count = 0; - - void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); -#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index fc6707e29..33a4e192a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -99,7 +99,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN DISCOVERY, #endif #ifndef UI_NO_HIBERNATE @@ -115,8 +115,10 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; +#if UI_DISCOVER_SCREEN + DiscoveredNode discovered[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) #endif @@ -206,6 +208,40 @@ public: : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false), sensors_lpp(200) { } +#if UI_DISCOVER_SCREEN + bool sendDiscoverRequest() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + the_mesh.getRNG()->random((uint8_t *) &disc_node_req_tag, 4); // generate random tag + memcpy(&cmd_bytes[2], &disc_node_req_tag, 4); + disc_nodes_count = 0; + mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + the_mesh.sendZeroHop(req); + discovery_req_time = millis(); + return true; + } + return false; + } + + void handleDiscoverResponse(const mesh::Packet* packet) { + if (disc_nodes_count < DISCOVERED_NODES_TABLE_SIZE && memcmp(&packet->payload[2], &disc_node_req_tag, 4) == 0) { + auto d = &discovered[disc_nodes_count++]; + memcpy(d->pubkey_prefix, &packet->payload[6], 8); + d->type = packet->payload[0] & 0xF; + d->snr_out = ((int8_t)packet->payload[1]) / 4.0; + d->snr_in = radio_driver.getLastSNR(); + ContactInfo* c = the_mesh.lookupContactByPubKey(&packet->payload[6], 8); + if (c != NULL) { + strncpy(d->name, c->name, 32); + } else { + d->name[0] = 0; + } + } + } +#endif + void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); @@ -464,9 +500,9 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN } else if (_page == HomePage::DISCOVERY) { - int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + int count = disc_nodes_count; display.setColor(UIColor::primary_txt); int y = 20; for (int i = 0; i < count; i++, y += 11) { @@ -493,8 +529,8 @@ public: } if (millis() < discovery_req_time + 5000) { return 1000; // more frequent updates just after req - } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc - y = 10 + 11 * UI_RECENT_LIST_SIZE; + } else if (count < DISCOVERED_NODES_TABLE_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * DISCOVERED_NODES_TABLE_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } #endif @@ -525,7 +561,7 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } @@ -562,11 +598,10 @@ public: return true; } #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter - the_mesh.requestRepeatersDiscovery(); - discovery_req_time = millis(); + sendDiscoverRequest(); } return true; } @@ -751,8 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { - ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); if (_display != NULL) { @@ -802,6 +840,14 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +#if UI_DISCOVER_SCREEN + if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + ((HomeScreen *) home)->handleDiscoverResponse(packet); + } +#endif +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index e12009538..842b1ac97 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,26 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifndef UI_DISCOVER_SCREEN + #define UI_DISCOVER_SCREEN 1 +#endif + +#if UI_DISCOVER_SCREEN + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif + +struct DiscoveredNode { + float snr_in; + float snr_out; + uint8_t pubkey_prefix[9]; + uint8_t type; + char name[32]; +}; +#endif + class UITask : public AbstractUITask { DisplayDriver* _display; SensorManager* _sensors; @@ -91,10 +111,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 08c845874..45a81da90 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,15 +139,18 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif if (path_len == 0xFF) { - sprintf(_origin, "(F) %s", from_name); + sprintf(_origin, "(F) %s", from.name); } else { - sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name); + sprintf(_origin, "(%d) %s", (uint32_t) path_len, from.name); } StrHelper::strncpy(_msg, text, sizeof(_msg)); @@ -197,6 +200,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 381414791..553dc7d0e 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,10 +71,11 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 8a8b77b53..71a8bb212 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,10 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -546,6 +549,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 925e8d719..48c697b9f 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,10 +99,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From cd54ee9754a0f77d31a69bb99088aec78cd5ee7c Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 17 Sep 2026 15:52:46 +1200 Subject: [PATCH 104/127] add missing null display driver class for thinknode m7 ethernet variant --- variants/thinknode_m7/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 5606d4822..69b38e33e 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -169,6 +169,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} From 30dd723c26cee71fc43dc54abb56cbf17d431a4e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 14:25:41 +1000 Subject: [PATCH 105/127] * MyMesh::begin() refactor, removing last display concepts --- examples/companion_radio/MyMesh.cpp | 24 ++++------------ examples/companion_radio/MyMesh.h | 3 +- examples/companion_radio/main.cpp | 43 +++++++++++++++-------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6e570ea50..409fed9f0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -911,7 +911,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe #endif } -void MyMesh::begin(bool has_display) { +void MyMesh::begin() { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { @@ -963,24 +963,7 @@ void MyMesh::begin(bool has_display) { _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours -#ifdef BLE_PIN_CODE // 123456 by default - if (_prefs.ble_pin == 0) { -#ifdef DISPLAY_CLASS - if (has_display && BLE_PIN_CODE == 123456) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } -#else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin -#endif - } else { - _active_ble_pin = _prefs.ble_pin; - } -#else - _active_ble_pin = 0; -#endif + _active_ble_pin = _prefs.ble_pin; resetContacts(); _store->loadContacts(this); @@ -1007,6 +990,9 @@ NodePrefs *MyMesh::getNodePrefs() { uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +void MyMesh::setBLEPin(uint32_t active_pin) { + _active_ble_pin = active_pin; +} struct FreqRange { uint32_t lower_freq, upper_freq; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f11286249..7bfbb2928 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -100,13 +100,14 @@ public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); - void begin(bool has_display); + void begin(); void startInterface(BaseSerialInterface &serial); void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); + void setBLEPin(uint32_t active_pin); void loop(); void handleCmdFrame(size_t len); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 857acf451..efc31779b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -172,37 +172,38 @@ void setup() { #endif #endif store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #else #error "need to define filesystem" #endif +#ifdef BLE_PIN_CODE // 123456 by default + if (the_mesh.getNodePrefs()->ble_pin == 0) { +#ifdef DISPLAY_CLASS + if (disp != NULL && BLE_PIN_CODE == 123456) { + StdRNG rng; + the_mesh.setBLEPin(rng.nextInt(100000, 999999)); // random pin each session + } else { + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin + } +#else + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin +#endif + } else { + the_mesh.setBLEPin(the_mesh.getNodePrefs()->ble_pin); + } +#else + the_mesh.setBLEPin(0); +#endif + // add bluetooth interface #if defined(BLE_PIN_CODE) bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); From b3b17025e38c3c486fa414f7bf31d26f8686ec5b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 17:33:36 +1000 Subject: [PATCH 106/127] * more MyMesh::Listener interface hooks --- examples/companion_radio/MyMesh.cpp | 20 +++++++++++++++++++- examples/companion_radio/MyMesh.h | 15 ++++++++++++--- examples/companion_radio/ui-new/UITask.cpp | 2 +- examples/companion_radio/ui-new/UITask.h | 2 +- examples/companion_radio/ui-orig/UITask.cpp | 5 +---- examples/companion_radio/ui-orig/UITask.h | 3 +-- examples/companion_radio/ui-tiny/UITask.cpp | 5 +---- examples/companion_radio/ui-tiny/UITask.h | 3 +-- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 409fed9f0..61288a6e8 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -415,6 +415,11 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { } ContactInfo* MyMesh::processAck(const uint8_t *data) { + if (_listener) { + uint32_t ack_crc; + memcpy(&ack_crc, data, 4); + _listener->onACKRecv(ack_crc); + } // see if matches any in a table for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient @@ -588,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(channel_details, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -626,6 +631,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } + if (_listener) { + _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + } } uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, @@ -673,6 +681,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim memcpy(&reply[4], telemetry.getBuffer(), tlen); return 4 + tlen; } + } else if (_listener) { + return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply); } return 0; // unknown } @@ -748,6 +758,8 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (_listener && len > 4) { + _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); } } @@ -822,6 +834,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); } + if (_listener) { + _listener->onRawDataRecv(packet); + } } void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, @@ -852,6 +867,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } + if (_listener) { + _listener->onTraceRecv(packet, tag, auth_code, flags, path_snrs, path_hashes, path_len); + } } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 7bfbb2928..667b19e19 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -92,10 +92,19 @@ public: class Listener { public: virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; - virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; - virtual void onControlDataRecv(const mesh::Packet* packet) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } + virtual void onControlDataRecv(const mesh::Packet* packet) { } + virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + const uint8_t *data, size_t data_len) { } + virtual void onACKRecv(uint32_t ack_crc) { } + virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } + virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } + virtual void onRawDataRecv(mesh::Packet *packet) { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 33a4e192a..8457367a2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -808,7 +808,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 842b1ac97..47928a171 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -112,7 +112,7 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; void onControlDataRecv(const mesh::Packet* packet) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 45a81da90..42e4ab79b 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -168,7 +168,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -200,9 +200,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 553dc7d0e..bb879cb63 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -72,10 +72,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 71a8bb212..607ac89c4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -549,9 +549,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 48c697b9f..83591d899 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -100,10 +100,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From 1077331e3162e8e80c76b900a0c8275d4fed2465 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Tue, 8 Sep 2026 16:57:12 -0400 Subject: [PATCH 107/127] Extend WiFi disconnect logging to specify reason Today if a user builds firmware with an incorrect WiFi SSID or password, they get a generic `WiFi disconnected.` error message. Extend logging to descirbe the reason that WiFi was disconnected. I gathered the updated logs for the 3 scenarios below. ``` WiFi: Attempting manual WiFi reconnect... WiFi: WiFi disconnected (reason=ASSOC_LEAVE). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... ``` ``` WiFi: WiFi disconnected (reason=NO_AP_FOUND). Flagging for reconnect... ``` ``` WiFi: WiFi connected successfully! WiFi: Got connection ``` --- examples/companion_radio/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc3..0e9899992 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -229,7 +229,8 @@ void setup() { WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...", + WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason)); wifi_needs_reconnect = true; } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); From 8d2a5657accd311e3f75578c46882313bdd4ec65 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Thu, 17 Sep 2026 22:12:53 -0400 Subject: [PATCH 108/127] Pin Radiolab for stm32 and replace ltoa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The major rev from 19.x to 20.x breaks Radiolab, so pin it for now. This should fix the failing wio-e5-mini_repeater builds. ltoa is removed upstream, so it was replaced with snprintf. The p[] buffer is always 16 bytes. Copy/paste for why claude thinks this is fine below, but also if this is broken it wouldn't be new. ``` exp2 in [23, 30] (int_part = mantissa << (exp2 - 23)): this is where int_part can reach its true max, up to 2,147,483,520 — 10 digits. That's what the snprintf bound of 11 (10 digits + NUL) is sized for. But in this branch frac_part is never assigned anything other than its initial 0, so only *p++ = '0' runs for the fraction — 1 char, not 7. exp2 in [0, 22] (int_part = mantissa >> (23 - exp2)): this is the only branch where frac_part can also be nonzero and enter the 7-digit loop. Here int_part < 2^23 by construction, so it's capped at 7 digits, never 10. ``` --- platformio.ini | 4 +++- src/helpers/TxtDataHelpers.cpp | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index de4d6c29c..f296f8382 100644 --- a/platformio.ini +++ b/platformio.ini @@ -110,7 +110,9 @@ build_flags = ${arduino_base.build_flags} [stm32_base] extends = arduino_base -platform = ststm32 +; Pinned pending upstream fix for upstream conversions. +; https://github.com/jgromes/RadioLib/issues/1873 +platform = ststm32@19.7.1 extra_scripts = post:arch/stm32/build_hex.py build_flags = ${arduino_base.build_flags} -D STM32_PLATFORM diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index d327931fd..f2621c0de 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -27,8 +27,9 @@ bool StrHelper::isBlank(const char* str) { } #include +#include -union int32_Float_t +union int32_Float_t { int32_t Long; float Float; @@ -100,11 +101,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '-'; if (int_part == 0) *p++ = '0'; - else + else { - ltoa(int_part, p, 10); - while (*p) - p++; + p += snprintf(p, 11, "%ld", (long)int_part); } *p++ = '.'; if (frac_part == 0) From 242e401a2c9f5b88980a170fba63b42a571081d3 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 18 Sep 2026 18:17:52 +1200 Subject: [PATCH 109/127] added reboot, shutdown and poweroff to companion cli commands --- examples/companion_radio/MyMesh.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01a7473a0..c38fa70ae 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2146,6 +2146,16 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "reboot") == 0) { + board.reboot(); // doesn't return + return true; + } + + if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) { + board.powerOff(); // doesn't return + return true; + } + if (memcmp(command, "set name ", 9) == 0) { if (AdvertDataParser::isValidName(&command[9])) { StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); From 843598724c34ee6857dec63d58383f5d49156303 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 18 Sep 2026 19:03:20 +1000 Subject: [PATCH 110/127] * fix for X1 --- examples/companion_radio/ui-orig/UITask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 42e4ab79b..a717a5401 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -408,7 +408,7 @@ void UITask::userLedHandler() { statusLedWrite(255, 0, 0); // red: battery low } else if (_msgcount > 0) { statusLedWrite(255, 90, 0); // amber: unread messages - } else if (_connected) { + } else if (hasConnection()) { statusLedWrite(0, 0, 255); // blue: app connected } else { statusLedWrite(0, 255, 0); // green: heartbeat From c425648bfc56db6103536426dc155274093c3898 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 19 Sep 2026 11:26:42 +0700 Subject: [PATCH 111/127] Off LED before powering off for repeater --- examples/simple_repeater/UITask.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557d..e0b6a41df 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -140,8 +140,10 @@ void UITask::loop() { } if (_powering_off_at > 0) { // power off timer armed -#ifdef LED_PIN +#if defined(LED_PIN) && defined(LED_STATE_ON) digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff + delay(1000); + digitalWrite(LED_PIN, !LED_STATE_ON); // off LED #endif if (millis() > _powering_off_at) { _board->powerOff(); // should not return From 64434c5326b080283a5576ef1601addc5bae52f4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 19 Sep 2026 20:13:18 +1000 Subject: [PATCH 112/127] * misc --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/MyMesh.h | 13 +++++++------ examples/companion_radio/ui-new/UITask.cpp | 8 +++++--- examples/companion_radio/ui-new/UITask.h | 10 +++++++--- examples/companion_radio/ui-orig/UITask.cpp | 6 ++++-- examples/companion_radio/ui-orig/UITask.h | 4 ++-- examples/companion_radio/ui-tiny/UITask.cpp | 4 ++-- examples/companion_radio/ui-tiny/UITask.h | 4 ++-- 8 files changed, 32 insertions(+), 23 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 61288a6e8..e1b031a92 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -473,7 +473,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe } if (_listener) { - _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); + _listener->onMessageRecv(pkt, from, txt_type, sender_timestamp, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -593,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMessageRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(pkt, channel_details, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -632,7 +632,7 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * _serial->writeFrame(frame, 1); } if (_listener) { - _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + _listener->onChannelDataRecv(pkt, channel, data_type, data, data_len); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 667b19e19..9f077c30f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -91,20 +91,21 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) = 0; + virtual void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } - virtual void onControlDataRecv(const mesh::Packet* packet) { } - virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + virtual void onControlDataRecv(const mesh::Packet* pkt) { } + virtual void onChannelDataRecv(mesh::Packet *pkt, const mesh::GroupChannel &channel, uint16_t data_type, const uint8_t *data, size_t data_len) { } virtual void onACKRecv(uint32_t ack_crc) { } virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } - virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } - virtual void onRawDataRecv(mesh::Packet *packet) { } + virtual void onRawDataRecv(mesh::Packet *pkt) { } + virtual ~Listener() { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8457367a2..181bc8328 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -786,10 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); @@ -808,7 +809,8 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); @@ -842,7 +844,7 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path void UITask::onControlDataRecv(const mesh::Packet* packet) { #if UI_DISCOVER_SCREEN - if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + if (packet->payload_len >= 14 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { ((HomeScreen *) home)->handleDiscoverResponse(packet); } #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 47928a171..361b04d01 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,10 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifdef UI_NO_DISCOVER_SCREEN + #error "UI_NO_DISCOVER_SCREEN is obsolete - use -D UI_DISCOVER_SCREEN=0 instead" +#endif + #ifndef UI_DISCOVER_SCREEN #define UI_DISCOVER_SCREEN 1 #endif @@ -111,11 +115,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; + void onControlDataRecv(const mesh::Packet* pkt) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index a717a5401..db8772f46 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,7 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -147,6 +147,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", from.name); } else { @@ -168,11 +169,12 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", channel_details.name); } else { diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index bb879cb63..d5f798986 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,8 +71,8 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 607ac89c4..7b70f8368 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,7 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 83591d899..084919aa6 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,8 +99,8 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; From 73afe3a0f548fc1df65c2afff4ae9eea1a1def54 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 14:22:13 +1200 Subject: [PATCH 113/127] allow fetching configured wifi password via companion cli --- examples/companion_radio/MyMesh.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c38fa70ae..79b4a8cef 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2191,6 +2191,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi.pwd updated (reboot to apply)"); return true; } + if (strcmp(command, "get wifi.pwd") == 0) { + sprintf(reply, "> %s", _prefs.wifi_pwd); + return true; + } if (strcmp(command, "set wifi.clear") == 0) { _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; @@ -2198,7 +2202,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi config cleared (reboot to apply)"); return true; } - if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + if (strcmp(command, "get wifi.ssid") == 0) { sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); return true; } From 4021a341f8bed9250f3bd145de9412b179f1dcdb Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 15:11:21 +1200 Subject: [PATCH 114/127] add wifi support to thinknode m7 ble variant --- variants/thinknode_m7/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 69b38e33e..128b12441 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -98,11 +98,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> From 798ce636df392572fea3186f5382b44fc4d1c486 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:47:24 -0700 Subject: [PATCH 115/127] Address review feedback on Muzi Base support - rename the board class and files to MuziBaseBoard - uppercase the build flags (MUZI_BASE, MUZI_BASE_SUPERIO) - name the superIO envs muzi_base_{duo,uno}_superIO_companion_radio_ble so build.sh picks them up - set MAX_CONTACTS to 350 like the other variants - move the pin setup from initVariant() into MuziBaseBoard::begin() --- .../{muzi_baseBoard.cpp => MuziBaseBoard.cpp} | 20 ++++++++++--- .../{muzi_baseBoard.h => MuziBaseBoard.h} | 4 +-- variants/muzi_base/platformio.ini | 22 +++++++-------- variants/muzi_base/target.cpp | 2 +- variants/muzi_base/target.h | 6 ++-- variants/muzi_base/variant.cpp | 28 ------------------- 6 files changed, 33 insertions(+), 49 deletions(-) rename variants/muzi_base/{muzi_baseBoard.cpp => MuziBaseBoard.cpp} (69%) rename variants/muzi_base/{muzi_baseBoard.h => MuziBaseBoard.h} (91%) diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/MuziBaseBoard.cpp similarity index 69% rename from variants/muzi_base/muzi_baseBoard.cpp rename to variants/muzi_base/MuziBaseBoard.cpp index ed713fa07..a104c550b 100644 --- a/variants/muzi_base/muzi_baseBoard.cpp +++ b/variants/muzi_base/MuziBaseBoard.cpp @@ -1,7 +1,7 @@ #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { @@ -10,7 +10,7 @@ const PowerMgtConfig power_config = { .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK }; -void muzi_baseBoard::initiateShutdown(uint8_t reason) { +void MuziBaseBoard::initiateShutdown(uint8_t reason) { // Disable LoRa module power before shutdown if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { @@ -21,14 +21,26 @@ void muzi_baseBoard::initiateShutdown(uint8_t reason) { } #endif // NRF52_POWER_MANAGEMENT -void muzi_baseBoard::begin() { +void MuziBaseBoard::begin() { NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); -#ifdef muzi_base_superIO + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + // output latches default to HIGH, so pull these low right after enabling + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // 12V rail is only needed for the superIO display pinMode(SCREEN_12V_ENABLE, OUTPUT); +#ifdef MUZI_BASE_SUPERIO digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display delay(250); +#else + digitalWrite(SCREEN_12V_ENABLE, LOW); #endif Wire.begin(); // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/MuziBaseBoard.h similarity index 91% rename from variants/muzi_base/muzi_baseBoard.h rename to variants/muzi_base/MuziBaseBoard.h index e5412b6a8..a5a4abbe2 100644 --- a/variants/muzi_base/muzi_baseBoard.h +++ b/variants/muzi_base/MuziBaseBoard.h @@ -15,14 +15,14 @@ #define MUZI_BASE_MFR_NAME "Muzi Base Duo" #endif -class muzi_baseBoard : public NRF52BoardDCDC { +class MuziBaseBoard : public NRF52BoardDCDC { protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif public: - muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + MuziBaseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} void begin(); #define BATTERY_SAMPLES 8 diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index eb35d5696..46a0ae4dc 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -17,7 +17,7 @@ build_flags = ${nrf52_base.build_flags} -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 -I variants/muzi_base -I src/helpers/ui - -D muzi_base + -D MUZI_BASE -D NRF52_POWER_MANAGEMENT -D PIN_USER_BTN=PIN_BUTTON1 -D USER_BTN_PRESSED=LOW @@ -106,7 +106,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -125,7 +125,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -150,7 +150,7 @@ extends = muzi_base_duo board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -174,12 +174,12 @@ lib_deps = ${muzi_base_duo.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_duo_companion_radio_ble_superIO] +[env:muzi_base_duo_superIO_companion_radio_ble] extends = muzi_base_duo_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 @@ -235,7 +235,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -254,7 +254,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -279,7 +279,7 @@ extends = muzi_base_uno board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -303,12 +303,12 @@ lib_deps = ${muzi_base_uno.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_uno_companion_radio_ble_superIO] +[env:muzi_base_uno_superIO_companion_radio_ble] extends = muzi_base_uno_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp index 2af0c7107..5bd846c98 100644 --- a/variants/muzi_base/target.cpp +++ b/variants/muzi_base/target.cpp @@ -3,7 +3,7 @@ #include "target.h" #include "variant.h" -muzi_baseBoard board; +MuziBaseBoard board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h index cbcfff141..45cd595fd 100644 --- a/variants/muzi_base/target.h +++ b/variants/muzi_base/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #if defined(USE_LR1121) #include #elif defined(USE_SX1262) @@ -19,7 +19,7 @@ #include -#ifdef muzi_base_superIO +#ifdef MUZI_BASE_SUPERIO #include extern DISPLAY_CLASS display; extern MomentaryButton user_btn; @@ -32,7 +32,7 @@ extern MomentaryButton user_btn; #endif -extern muzi_baseBoard board; +extern MuziBaseBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp index b179af8ac..cbde3355b 100644 --- a/variants/muzi_base/variant.cpp +++ b/variants/muzi_base/variant.cpp @@ -60,31 +60,3 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 46, 47, }; - -void initVariant() -{ - // All pins output HIGH by default. - // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 - - pinMode(PIN_VBAT_READ, INPUT); - pinMode(PIN_BATTERY_CHARGING, INPUT); - pinMode(PIN_CHARGER_FAULT, INPUT); - pinMode(PIN_BUTTON1, INPUT); - pinMode(PIN_BUTTON2, INPUT); - pinMode(PIN_BUTTON3, INPUT); - pinMode(PIN_BUTTON4, INPUT); - pinMode(PIN_BUTTON5, INPUT); - pinMode(PIN_BUTTON6, INPUT); - pinMode(LED_PIN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_PIN, LOW); - digitalWrite(LED_BLUE, LOW); - pinMode(BUZZER_PIN, OUTPUT); - digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. - // gps power is driven by the sensor manager (mode switch). off to start. - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); - - pinMode(SCREEN_12V_ENABLE, OUTPUT); - digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. -} From 114cf9f4fcd0a1486ce01fa4bbf6dd6575a217f3 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:46:25 -0700 Subject: [PATCH 116/127] Use the default BLE TX power on Muzi Base Drop BLE_TX_POWER=0 from the companion envs so they use the default of 4 dBm, like most other variants. The lower power was carried over from the original Base Duo PR and real-world range is better at the default. --- variants/muzi_base/platformio.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index 46a0ae4dc..72a3ecac2 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -128,7 +128,6 @@ build_flags = ${muzi_base_duo.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -183,7 +182,6 @@ build_flags = ${muzi_base_duo_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -257,7 +255,6 @@ build_flags = ${muzi_base_uno.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -312,7 +309,6 @@ build_flags = ${muzi_base_uno_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 From 12a8c8d4d42bf42189526a02906d328a893f2177 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Sun, 20 Sep 2026 15:16:33 -0400 Subject: [PATCH 117/127] Add missing include, and revert removal of lota --- src/helpers/TxtDataHelpers.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index f2621c0de..4358c8b83 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -28,6 +28,10 @@ bool StrHelper::isBlank(const char* str) { #include #include +#if __has_include() +// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084 +#include +#endif union int32_Float_t { @@ -103,7 +107,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '0'; else { - p += snprintf(p, 11, "%ld", (long)int_part); + ltoa(int_part, p, 10); + while (*p) + p++; } *p++ = '.'; if (frac_part == 0) From 0a15c487d8b8333dfdc5324deca1a56bb8c8eba0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 29 Aug 2026 17:57:06 -0400 Subject: [PATCH 118/127] m4: inital port --- boards/thinknode_m4.json | 72 ++++++++++++ variants/thinknode_m4/ThinkNodeM4Board.cpp | 46 ++++++++ variants/thinknode_m4/ThinkNodeM4Board.h | 61 ++++++++++ variants/thinknode_m4/platformio.ini | 130 +++++++++++++++++++++ variants/thinknode_m4/target.cpp | 84 +++++++++++++ variants/thinknode_m4/target.h | 26 +++++ variants/thinknode_m4/variant.cpp | 94 +++++++++++++++ variants/thinknode_m4/variant.h | 106 +++++++++++++++++ 8 files changed, 619 insertions(+) create mode 100644 boards/thinknode_m4.json create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.cpp create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.h create mode 100644 variants/thinknode_m4/platformio.ini create mode 100644 variants/thinknode_m4/target.cpp create mode 100644 variants/thinknode_m4/target.h create mode 100644 variants/thinknode_m4/variant.cpp create mode 100644 variants/thinknode_m4/variant.h diff --git a/boards/thinknode_m4.json b/boards/thinknode_m4.json new file mode 100644 index 000000000..babc3e52f --- /dev/null +++ b/boards/thinknode_m4.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow nrf", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/variants/thinknode_m4/ThinkNodeM4Board.cpp b/variants/thinknode_m4/ThinkNodeM4Board.cpp new file mode 100644 index 000000000..4a144a3a4 --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.cpp @@ -0,0 +1,46 @@ +#include +#include "ThinkNodeM4Board.h" +#include + +#include + +void ThinkNodeM4Board::begin() { + NRF52Board::begin(); + btn_prev_state = HIGH; + + Wire.begin(); + battery_serial->begin(4800); + + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM4Board::getBattMilliVolts() { + int tentatives = 10; + uint8_t data[5]; + uint8_t b; + + if (battery_serial->available() < 10) + return bat_level_mv; + + // discard old data + while (battery_serial->available() > 10) + battery_serial->read(); + + // synchronize + while((b = battery_serial->read()) != 0xFE) { + if (tentatives-- == 0) { + MESH_DEBUG_PRINTLN("Could not find battery frame start %x", b); + return bat_level_mv; + } + } + battery_serial->readBytes(data, 5); + + if (data[4] != 0xFD) { + MESH_DEBUG_PRINTLN("Invalid battery frame end %x", data[4]); + return bat_level_mv; + } + bat_level_percent = data[0]; + //MESH_DEBUG_PRINTLN("Battery level %d\%", bat_level_percent); + bat_level_mv = 2 * (data[1]*1000. + data[2]*10. + data[3]/10.); + return bat_level_mv; +} diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h new file mode 100644 index 000000000..ba7d95be6 --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +class ThinkNodeM4Board : public NRF52BoardDCDC { +protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + uint8_t btn_prev_state; + HardwareSerial * battery_serial; + uint16_t bat_level_mv = 0; + uint8_t bat_level_percent =0; + +public: + ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + void begin(); + uint16_t getBattMilliVolts() override; + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off + } +#endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode M4"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == USER_BTN_PRESSED) ? 1 : -1; + } + #endif + return 0; + } + + void shutdownPeripherals() override { + // shutdown common peripherals + NRF52Board::shutdownPeripherals(); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); + digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + } +}; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini new file mode 100644 index 000000000..ab8417e99 --- /dev/null +++ b/variants/thinknode_m4/platformio.ini @@ -0,0 +1,130 @@ +[ThinkNode_M4] +extends = nrf52_base +board = thinknode_m4 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m4 + -I src/helpers/ui + -D THINKNODE_M4 + -D PIN_USER_BTN=12 + -D PIN_STATUS_LED=LED_STATUS + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D P_LORA_BUSY=26 + -D P_LORA_SCLK=6 + -D P_LORA_NSS=27 + -D P_LORA_DIO_1=12 + -D P_LORA_MISO=8 + -D P_LORA_MOSI=7 + -D P_LORA_RESET=40 + -D P_LORA_TX_LED=LED_TX + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 + -D ENV_INCLUDE_GPS=1 +; -D MESH_DEBUG=1 +; -D GPS_NMEA_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/thinknode_m4> + + +debug_tool = stlink +upload_protocol = nrfutil +lib_deps= ${nrf52_base.lib_deps} + +[env:ThinkNode_M4_repeater] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_room_server] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RF_SWITCH_TABLE +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_companion_radio_usb] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_companion_radio_ble] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_kiss_modem] +extends = ThinkNode_M4 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 diff --git a/variants/thinknode_m4/target.cpp b/variants/thinknode_m4/target.cpp new file mode 100644 index 000000000..7da3f400d --- /dev/null +++ b/variants/thinknode_m4/target.cpp @@ -0,0 +1,84 @@ +#include +#include "target.h" +#include + +ThinkNodeM4Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +#ifdef RF_SWITCH_TABLE +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW , LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW , HIGH }}, + { LR11x0::MODE_TX_HF, {LOW , LOW }}, + { LR11x0::MODE_GNSS, {LOW , LOW }}, + { LR11x0::MODE_WIFI, {LOW , LOW }}, + END_OF_MODE_TABLE, +}; +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/thinknode_m4/target.h b/variants/thinknode_m4/target.h new file mode 100644 index 000000000..1135f5e89 --- /dev/null +++ b/variants/thinknode_m4/target.h @@ -0,0 +1,26 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "ThinkNodeM4Board.h" +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern ThinkNodeM4Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp new file mode 100644 index 000000000..7bf17bb86 --- /dev/null +++ b/variants/thinknode_m4/variant.cpp @@ -0,0 +1,94 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02 + 3, // P0.03 + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 + 12, // P0.12 + 13, // P0.13 + 14, // P0.14 + 15, // P0.15 + 16, // P0.16 + 17, // P0.17 + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24 + 25, // P0.25 + 26, // P0.26 + 27, // P0.27 + 28, // P0.28 + 29, // P0.29 + 30, // P0.30 + 31, // P0.31 + 32, // P1.00 + 33, // P1.01 + 34, // P1.02 + 35, // P1.03 + 36, // P1.04 + 37, // P1.05 + 38, // P1.06 + 39, // P1.07 + 40, // P1.08 + 41, // P1.09 + 42, // P1.10 + 43, // P1.11 + 44, // P1.12 + 45, // P1.13 + 46, // P1.14 + 47, // P1.15 +}; + +void initVariant() +{ +/* TODO */ + pinMode(PIN_PWR_EN, OUTPUT); + pinMode(I2C_POWER, OUTPUT); + + digitalWrite(PIN_PWR_EN, HIGH); + digitalWrite(I2C_POWER, I2C_POWER_ACTIVE); + + pinMode(LED_STATUS, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BAT1, OUTPUT); + pinMode(LED_BAT2, OUTPUT); + pinMode(LED_BAT3, OUTPUT); + pinMode(LED_BAT4, OUTPUT); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + pinMode(BUTTON_PIN, INPUT_PULLUP); + + pinMode(PIN_GPS_POWER, OUTPUT); + pinMode(PIN_GPS_EN, OUTPUT); + + // Power on gps but in standby + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); +} diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h new file mode 100644 index 000000000..cc4831840 --- /dev/null +++ b/variants/thinknode_m4/variant.h @@ -0,0 +1,106 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power + +#define EXT_CHRG_DETECT (38) +// Power to radio +#define PIN_PWR_EN (11) + +// I2C bus power +#define I2C_POWER (32) +#define I2C_POWER_ACTIVE LOW + +#define PIN_BAT_RX (5) +#define PIN_BAT_TX (30) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX PIN_BAT_TX +#define PIN_SERIAL2_TX PIN_BAT_RX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) +#define I2C_NO_RESCAN + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) +#define PIN_SPI_NSS (27) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BLUE (-1) // disable blue led +#define LED_STATUS (13) // blue +#define LED_PIN (41) // red +#define LED_TX LED_PIN +#define LED_BUILTIN LED_BLUE +#define LED_STATE_ON HIGH + +#define LED_BAT1 (15) +#define LED_BAT2 (17) +#define LED_BAT3 (34) +#define LED_BAT4 (36) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (4) +#define BUTTON_PIN PIN_BUTTON1 +#define USER_BTN_PRESSED HIGH + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define PIN_GPS_RX (44) +#define PIN_GPS_TX (46) + +#define PIN_GPS_POWER (14) +#define GPS_POWER_ACTIVE LOW +#define PIN_GPS_EN (43) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_ACTIVE HIGH From 8bf072a6f011d841cd6c0a4168aba85e5ad6d2ae Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 20 Sep 2026 16:13:15 -0400 Subject: [PATCH 119/127] M4: Handle poweroff --- variants/thinknode_m4/ThinkNodeM4Board.h | 14 ++++++++++++++ variants/thinknode_m4/platformio.ini | 2 +- variants/thinknode_m4/variant.h | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index ba7d95be6..c05de9115 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -47,6 +47,16 @@ public: // shutdown common peripherals NRF52Board::shutdownPeripherals(); + #ifdef LED_STATUS + digitalWrite(LED_STATUS, HIGH); + #endif + #ifdef BUTTON_PIN + while(!digitalRead(BUTTON_PIN)); + #endif + #ifdef LED_STATUS + digitalWrite(LED_STATUS, LOW); + #endif + digitalWrite(LED_BAT1, LOW); digitalWrite(LED_BAT2, LOW); digitalWrite(LED_BAT3, LOW); @@ -57,5 +67,9 @@ public: digitalWrite(PIN_PWR_EN, LOW); digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + #endif } }; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini index ab8417e99..06762f88d 100644 --- a/variants/thinknode_m4/platformio.ini +++ b/variants/thinknode_m4/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/thinknode_m4 -I src/helpers/ui -D THINKNODE_M4 - -D PIN_USER_BTN=12 + -D PIN_USER_BTN=4 -D PIN_STATUS_LED=LED_STATUS -D RADIO_CLASS=CustomLR1110 -D WRAPPER_CLASS=CustomLR1110Wrapper diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h index cc4831840..6e355d7f8 100644 --- a/variants/thinknode_m4/variant.h +++ b/variants/thinknode_m4/variant.h @@ -88,7 +88,7 @@ #define PIN_BUTTON1 (4) #define BUTTON_PIN PIN_BUTTON1 -#define USER_BTN_PRESSED HIGH +#define USER_BTN_PRESSED LOW //////////////////////////////////////////////////////////////////////////////// // GPS From 84f1e11d29823d51a0047beb07d76b9523093ce5 Mon Sep 17 00:00:00 2001 From: Ev Lbibass <27320050+lbibass@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:38:05 -0400 Subject: [PATCH 120/127] update structure of LR1121 to match LR1110 files. Implement DC-DC on LR1121 radio. saves about 30% power consumption at idle. --- src/helpers/radiolib/CustomLR1121.h | 74 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1121Wrapper.h | 3 + 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 873bfabc4..67e3b52a9 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -4,11 +4,25 @@ #include "MeshCore.h" class CustomLR1121 : public LR1121 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: CustomLR1121(Module *mod) : LR1121(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1121::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { @@ -31,11 +45,61 @@ class CustomLR1121 : public LR1121 { bool getRxBoostedGainMode() const { return _rx_boosted; } - bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags. + return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } - uint8_t getSpreadingFactor() const { return spreadingFactor; } + bool isReceiving() { + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + + uint8_t getSpreadingFactor() const { return spreadingFactor; } }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h index 5361ee238..5a754cee8 100644 --- a/src/helpers/radiolib/CustomLR1121Wrapper.h +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -14,6 +14,9 @@ public: ((CustomLR1121 *)_radio)->setBandwidth(bw); ((CustomLR1121 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } From b5787757a7666b311f016263fed280bd94c22b9e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 21 Sep 2026 15:55:44 +0200 Subject: [PATCH 121/127] Thinknode m4 - don't touch crystal and OTA name --- variants/thinknode_m4/ThinkNodeM4Board.h | 2 +- variants/thinknode_m4/variant.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index c05de9115..c2cae54b6 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -15,7 +15,7 @@ protected: uint8_t bat_level_percent =0; public: - ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + ThinkNodeM4Board() : NRF52Board("THINKNODE_M4_OTA"), battery_serial(&Serial2) {} void begin(); uint16_t getBattMilliVolts() override; diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp index 7bf17bb86..fbdb08e2a 100644 --- a/variants/thinknode_m4/variant.cpp +++ b/variants/thinknode_m4/variant.cpp @@ -10,8 +10,8 @@ const uint32_t g_ADigitalPinMap[] = { - 0, // P0.00 - 1, // P0.01 + 0xff, // P0.00 - LFXO (do not use) + 0xff, // P0.01 - LFXO (do not use) 2, // P0.02 3, // P0.03 4, // P0.04 From 0c3fe356d06ea6b9eb6df719c9647ee3664c821d Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Mon, 21 Sep 2026 12:26:36 -0400 Subject: [PATCH 122/127] Constrain `agc.reset.interval` and allow intervals > 255s --- docs/cli_commands.md | 2 +- examples/companion_radio/NodePrefs.h | 4 ++-- src/helpers/CommonCLI.h | 4 ++-- src/helpers/CommonRadioPrefs.cpp | 4 +++- src/helpers/CommonRadioPrefs.h | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f4a43fad3..9fde4d436 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -628,7 +628,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set agc.reset.interval ` **Parameters:** -- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable. +- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable. **Default:** `0.0` diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4b169d73f..46a3dbb7c 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -106,8 +106,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8591cdc14..2759b0c4f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -119,8 +119,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 9d92cad9a..41b179932 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -165,7 +165,9 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } if (memcmp(command, "set agc.reset.interval ", 23) == 0) { - setAgcResetInt(atoi(&command[23])); + int secs = atoi(&command[23]); + secs = constrain(secs, 0, 255 * 4); + setAgcResetInt((uint16_t) secs); sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); return true; } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index 96895bb2e..0e83ca9df 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -41,8 +41,8 @@ public: virtual float getRxDelay() const = 0; virtual void setRxDelay(float d) = 0; - virtual uint8_t getAgcResetInt() const = 0; - virtual void setAgcResetInt(uint8_t secs) = 0; + virtual uint16_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint16_t secs) = 0; virtual uint8_t getHashMode() const = 0; virtual void setHashMode(uint8_t m) = 0; From 61be70849a9a2f123b4b1e0146a1ee05a261ddb5 Mon Sep 17 00:00:00 2001 From: Wolfram Keil Date: Thu, 27 Aug 2026 21:38:46 +0200 Subject: [PATCH 123/127] Add board loop hook for periodic work --- examples/simple_repeater/main.cpp | 2 ++ examples/simple_sensor/main.cpp | 2 ++ src/MeshCore.h | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1f71da74d..d475b13d8 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -165,6 +165,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + #ifdef ETHERNET_ENABLED ethernet_loop_maintain(); if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 749ff6ef1..db1c5279f 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -153,6 +153,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/src/MeshCore.h b/src/MeshCore.h index 4bc9d3dbb..2a3582de0 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -76,6 +76,10 @@ public: virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } + + // Called from the example main loops. Lets a board feed its watchdog and + // run periodic housekeeping. Default no-op. + virtual void loop() { /* no op */ } }; /** From 560fb847d580a2cc97d29ab531c2a0273e20fefc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 22 Sep 2026 18:01:15 +1000 Subject: [PATCH 124/127] missing board.loop() in companion, room server, kiss modem. --- examples/companion_radio/main.cpp | 1 + examples/kiss_modem/main.cpp | 4 +++- examples/simple_room_server/main.cpp | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4bc75a81e..839d49583 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -297,6 +297,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 5836a6941..44b2a3df0 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,7 +113,7 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) && ARDUINO_USB_MODE +#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); #endif @@ -149,6 +149,8 @@ void loop() { } } + board.loop(); + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { radio_driver.triggerNoiseFloorCalibrate(0); next_noise_floor_calib_ms = millis(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 227ee2cbd..65fc55ef7 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -162,6 +162,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif From c13b31218869f11c8b6e47f16d96b9bab9329095 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 23 Sep 2026 15:04:51 +1200 Subject: [PATCH 125/127] fix heltec ct62 boot loop --- variants/heltec_ct62/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 401a30d23..f98f8d4a5 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -1,6 +1,7 @@ [Heltec_ct62] extends = esp32_base board = esp32-c3-devkitm-1 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32_base.build_flags} -I variants/heltec_ct62 From b204f9a114db7b30e166fcb4f6db3b30424f2f55 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 24 Sep 2026 13:49:57 +1000 Subject: [PATCH 126/127] * changed onUnhandledResponse() to return true if handled/consumed * unhandled responses (eg. push telemetry) now sent to app as PUSH_CODE_BINARY_RESPONSE frames --- examples/companion_radio/MyMesh.cpp | 17 +++++++++++++++-- examples/companion_radio/MyMesh.h | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 66efc6b0c..f32ea67ae 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -758,8 +758,21 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); - } else if (_listener && len > 4) { - _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); + } else { + bool handled; + if (_listener && len > 4) { + handled = _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); + } else { + handled = false; + } + if (!handled) { // let the app know about unknown/unhandled responses (eg. push telemetry packets) + int i = 0; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], data, len); + i += len; + _serial->writeFrame(out_frame, i); + } } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 9f077c30f..52a22cb41 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -101,7 +101,7 @@ public: virtual void onACKRecv(uint32_t ack_crc) { } virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } - virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual bool onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { return false; /* not handled */ } virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } virtual void onRawDataRecv(mesh::Packet *pkt) { } From aa1c46e1db1c24a828a896db8b7ed36047cf1515 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 25 Sep 2026 16:55:17 +1200 Subject: [PATCH 127/127] disable hibernate ui on 1watt wio tracker variant --- variants/wio-tracker-l1-1w/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini index e5b4a3be3..7d3c2dc39 100644 --- a/variants/wio-tracker-l1-1w/platformio.ini +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1-1W.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -96,6 +97,7 @@ build_flags = ${WioTrackerL1-1W.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D RADIO_FEM_TXGAIN=1