From c89a0e9929f4247d40569f568ccb1d216cc66762 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 22 Jan 2026 15:03:40 +0800 Subject: [PATCH 01/74] Add an external watchdog module --- examples/companion_radio/main.cpp | 7 ++++++ examples/simple_repeater/main.cpp | 12 +++++++++- examples/simple_room_server/main.cpp | 7 ++++++ examples/simple_secure_chat/main.cpp | 7 ++++++ examples/simple_sensor/main.cpp | 7 ++++++ src/helpers/ExWatchdogManager.h | 11 +++++++++ variants/heltec_mesh_solar/platformio.ini | 5 ++++ variants/heltec_mesh_solar/target.cpp | 28 +++++++++++++++++++++++ variants/heltec_mesh_solar/target.h | 11 +++++++++ variants/heltec_mesh_solar/variant.h | 4 ++-- 10 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/helpers/ExWatchdogManager.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 82c8c21d93..2035d78974 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -110,6 +110,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + #ifdef DISPLAY_CLASS DisplayDriver* disp = NULL; if (display.begin()) { @@ -228,4 +232,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8c745613e7..f371cb4651 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -29,6 +29,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + // For power saving lastActive = millis(); // mark last active time since boot @@ -124,11 +128,17 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); - +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif if (the_mesh.getNodePrefs()->powersaving_enabled && // To check if power saving is enabled the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep if (!the_mesh.hasPendingWork()) { // No pending work. Safe to sleep +#ifdef HAS_EX_WATCHDOG + board.sleep(ex_watchdog.getIntervalMs()>1800?1800:ex_watchdog.getIntervalMs()); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet +#else board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet +#endif lastActive = millis(); nextSleepinSecs = 5; // Default: To work for 5s and sleep again } else { diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 1a3b4d6e0f..e468b84e87 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -24,6 +24,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -111,4 +115,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif } diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da1bac5b32..693ebfeea6 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -559,6 +559,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + if (!radio_init()) { halt(); } fast_rng.begin(radio_get_rng_seed()); @@ -588,4 +592,7 @@ void setup() { void loop() { the_mesh.loop(); rtc_clock.tick(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index a5fcc1484f..9712a20724 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -58,6 +58,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -145,4 +149,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif } diff --git a/src/helpers/ExWatchdogManager.h b/src/helpers/ExWatchdogManager.h new file mode 100644 index 0000000000..65f178b0cd --- /dev/null +++ b/src/helpers/ExWatchdogManager.h @@ -0,0 +1,11 @@ +#pragma once + +class ExWatchdogManager { +public: + unsigned long next_feed_watchdog; + ExWatchdogManager() { next_feed_watchdog = 0; } + virtual bool begin() { return false; } + virtual void loop() { } + virtual unsigned long getIntervalMs() const { return 0; } + virtual void feed() { } +}; diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index 7bfbac85a7..3eb51f0a59 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -14,6 +14,11 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D HAS_EX_WATCHDOG + -D EX_WATCHDOG_DONE_PIN=9 + -D EX_WATCHDOG_WAKE_PIN=10 + -D EX_WATCHDOG_TIMEOUT_MS=480000 ;(6*60*1000) ; 6 minute watchdog + build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/heltec_mesh_solar> diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index ad79f71712..060f20c38f 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -13,6 +13,7 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); SolarSensorManager sensors = SolarSensorManager(nmea); +SolarExWatchdog ex_watchdog; #ifdef DISPLAY_CLASS DISPLAY_CLASS display; @@ -121,3 +122,30 @@ bool SolarSensorManager::setSettingValue(const char* name, const char* value) { } return false; // not supported } + +bool SolarExWatchdog::begin() { + next_feed_watchdog = 0; + pinMode(EX_WATCHDOG_WAKE_PIN, INPUT); + pinMode(EX_WATCHDOG_DONE_PIN, OUTPUT); + delay(1); + digitalWrite(EX_WATCHDOG_DONE_PIN, LOW); + delay(1); + feed(); + return true; +} +void SolarExWatchdog::loop() { + if (millis() > next_feed_watchdog) { + feed(); + next_feed_watchdog = millis() + EX_WATCHDOG_TIMEOUT_MS; + } +} + +unsigned long SolarExWatchdog::getIntervalMs() const { + return next_feed_watchdog - millis(); +} + +void SolarExWatchdog::feed() { + digitalWrite(EX_WATCHDOG_DONE_PIN, HIGH); + delay(1); + digitalWrite(EX_WATCHDOG_DONE_PIN, LOW); +} \ No newline at end of file diff --git a/variants/heltec_mesh_solar/target.h b/variants/heltec_mesh_solar/target.h index e301a27351..c535f68ec8 100644 --- a/variants/heltec_mesh_solar/target.h +++ b/variants/heltec_mesh_solar/target.h @@ -8,6 +8,7 @@ #include #include #include +#include #ifdef DISPLAY_CLASS #include #endif @@ -30,10 +31,20 @@ class SolarSensorManager : public SensorManager { bool setSettingValue(const char* name, const char* value) override; }; +class SolarExWatchdog : public ExWatchdogManager { +public: + SolarExWatchdog() {} + bool begin() override; + void loop() override; + unsigned long getIntervalMs() const override; + void feed() override; +}; + extern MeshSolarBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern SolarSensorManager sensors; +extern SolarExWatchdog ex_watchdog; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; diff --git a/variants/heltec_mesh_solar/variant.h b/variants/heltec_mesh_solar/variant.h index 14956619f0..3c1b378dfa 100644 --- a/variants/heltec_mesh_solar/variant.h +++ b/variants/heltec_mesh_solar/variant.h @@ -34,8 +34,8 @@ #define PIN_SERIAL1_RX (37) #define PIN_SERIAL1_TX (39) -#define PIN_SERIAL2_RX (9) -#define PIN_SERIAL2_TX (10) +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) //////////////////////////////////////////////////////////////////////////////// // I2C pin definition From 53ff4ed57f69c38c5aae7e41e51aa1016cdf2016 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Mon, 9 Feb 2026 11:43:41 +0800 Subject: [PATCH 02/74] Fix watchdog code in repeater --- examples/simple_repeater/main.cpp | 23 ++++++++++++++++++++--- src/helpers/ExWatchdogManager.h | 3 ++- variants/heltec_mesh_solar/target.cpp | 9 +++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 6cc2ac77a8..1760abeaaf 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -29,6 +29,10 @@ void setup() { board.begin(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.begin(); +#endif + #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) // give some extra time for serial to settle so // boot debug messages can be seen on terminal @@ -134,11 +138,24 @@ void loop() { #endif rtc_clock.tick(); +#ifdef HAS_EX_WATCHDOG + ex_watchdog.loop(); +#endif if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { - #if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) +#ifdef HAS_EX_WATCHDOG + uint32_t sleep_interval = ex_watchdog.getIntervalMs()/1000; + board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet +#else + board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet +#endif board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible - #else +#else if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep +#ifdef HAS_EX_WATCHDOG + uint32_t sleep_interval = ex_watchdog.getIntervalMs()/1000; + board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet +#else board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet #endif lastActive = millis(); @@ -146,6 +163,6 @@ void loop() { } else { nextSleepinSecs += 5; // When there is pending work, to work another 5s } - #endif +#endif } } diff --git a/src/helpers/ExWatchdogManager.h b/src/helpers/ExWatchdogManager.h index 65f178b0cd..4df901b7f6 100644 --- a/src/helpers/ExWatchdogManager.h +++ b/src/helpers/ExWatchdogManager.h @@ -1,8 +1,9 @@ #pragma once class ExWatchdogManager { -public: +protected: unsigned long next_feed_watchdog; +public: ExWatchdogManager() { next_feed_watchdog = 0; } virtual bool begin() { return false; } virtual void loop() { } diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index c9172aa1a6..7eb44bfafb 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -141,11 +141,16 @@ void SolarExWatchdog::loop() { } unsigned long SolarExWatchdog::getIntervalMs() const { - return next_feed_watchdog - millis(); + unsigned long interval_ms = 0; + interval_ms = next_feed_watchdog - millis(); + if(interval_ms > EX_WATCHDOG_TIMEOUT_MS) { + interval_ms = EX_WATCHDOG_TIMEOUT_MS; + } + return interval_ms; } void SolarExWatchdog::feed() { digitalWrite(EX_WATCHDOG_DONE_PIN, HIGH); delay(1); digitalWrite(EX_WATCHDOG_DONE_PIN, LOW); -} \ No newline at end of file +} From 1ab8a6f6da2de8c94c94d1e29b93bdaeef02ef6a Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 4 Mar 2026 03:13:53 +0100 Subject: [PATCH 03/74] Address review comments --- examples/companion_radio/main.cpp | 8 +++--- examples/simple_repeater/main.cpp | 16 ++++++------ examples/simple_room_server/main.cpp | 8 +++--- examples/simple_secure_chat/main.cpp | 8 +++--- examples/simple_sensor/main.cpp | 8 +++--- ...dogManager.h => ExternalWatchdogManager.h} | 4 +-- variants/heltec_mesh_solar/platformio.ini | 8 +++--- variants/heltec_mesh_solar/target.cpp | 26 +++++++++---------- variants/heltec_mesh_solar/target.h | 8 +++--- 9 files changed, 47 insertions(+), 47 deletions(-) rename src/helpers/{ExWatchdogManager.h => ExternalWatchdogManager.h} (71%) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 3c69625a15..88ef392455 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -110,8 +110,8 @@ void setup() { board.begin(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.begin(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.begin(); #endif #ifdef DISPLAY_CLASS @@ -229,7 +229,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.loop(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); #endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1760abeaaf..f0271c8896 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -29,8 +29,8 @@ void setup() { board.begin(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.begin(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.begin(); #endif #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) @@ -138,13 +138,13 @@ void loop() { #endif rtc_clock.tick(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.loop(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); #endif if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) -#ifdef HAS_EX_WATCHDOG - uint32_t sleep_interval = ex_watchdog.getIntervalMs()/1000; +#ifdef HAS_EXTERNAL_WATCHDOG + uint32_t sleep_interval = external_watchdog.getIntervalMs()/1000; board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet #else board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet @@ -152,8 +152,8 @@ void loop() { board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible #else if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep -#ifdef HAS_EX_WATCHDOG - uint32_t sleep_interval = ex_watchdog.getIntervalMs()/1000; +#ifdef HAS_EXTERNAL_WATCHDOG + uint32_t sleep_interval = external_watchdog.getIntervalMs()/1000; board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet #else board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index bb6a860b82..d20ebc6d19 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -24,8 +24,8 @@ void setup() { board.begin(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.begin(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.begin(); #endif #ifdef DISPLAY_CLASS @@ -117,7 +117,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.loop(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); #endif } diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index a218223634..a21741db51 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -559,8 +559,8 @@ void setup() { board.begin(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.begin(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.begin(); #endif if (!radio_init()) { halt(); } @@ -594,7 +594,7 @@ void setup() { void loop() { the_mesh.loop(); rtc_clock.tick(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.loop(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); #endif } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index e9ff9387ea..346f4dfc99 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -58,8 +58,8 @@ void setup() { board.begin(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.begin(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.begin(); #endif #ifdef DISPLAY_CLASS @@ -151,7 +151,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); -#ifdef HAS_EX_WATCHDOG - ex_watchdog.loop(); +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); #endif } diff --git a/src/helpers/ExWatchdogManager.h b/src/helpers/ExternalWatchdogManager.h similarity index 71% rename from src/helpers/ExWatchdogManager.h rename to src/helpers/ExternalWatchdogManager.h index 4df901b7f6..9ef8abaa61 100644 --- a/src/helpers/ExWatchdogManager.h +++ b/src/helpers/ExternalWatchdogManager.h @@ -1,10 +1,10 @@ #pragma once -class ExWatchdogManager { +class ExternalWatchdogManager { protected: unsigned long next_feed_watchdog; public: - ExWatchdogManager() { next_feed_watchdog = 0; } + ExternalWatchdogManager() { next_feed_watchdog = 0; } virtual bool begin() { return false; } virtual void loop() { } virtual unsigned long getIntervalMs() const { return 0; } diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index 3eb51f0a59..1bc7e7fa7a 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -14,10 +14,10 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D HAS_EX_WATCHDOG - -D EX_WATCHDOG_DONE_PIN=9 - -D EX_WATCHDOG_WAKE_PIN=10 - -D EX_WATCHDOG_TIMEOUT_MS=480000 ;(6*60*1000) ; 6 minute watchdog + -D HAS_EXTERNAL_WATCHDOG + -D EXTERNAL_WATCHDOG_DONE_PIN=9 + -D EXTERNAL_WATCHDOG_WAKE_PIN=10 + -D EXTERNAL_WATCHDOG_TIMEOUT_MS=480000 ;(6*60*1000) ; 6 minute watchdog build_src_filter = ${nrf52_base.build_src_filter} + diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index 7eb44bfafb..fa2342026d 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -13,7 +13,7 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); SolarSensorManager sensors = SolarSensorManager(nmea); -SolarExWatchdog ex_watchdog; +SolarExternalWatchdog external_watchdog; #ifdef DISPLAY_CLASS DISPLAY_CLASS display; @@ -123,34 +123,34 @@ bool SolarSensorManager::setSettingValue(const char* name, const char* value) { return false; // not supported } -bool SolarExWatchdog::begin() { +bool SolarExternalWatchdog::begin() { next_feed_watchdog = 0; - pinMode(EX_WATCHDOG_WAKE_PIN, INPUT); - pinMode(EX_WATCHDOG_DONE_PIN, OUTPUT); + pinMode(EXTERNAL_WATCHDOG_WAKE_PIN, INPUT); + pinMode(EXTERNAL_WATCHDOG_DONE_PIN, OUTPUT); delay(1); - digitalWrite(EX_WATCHDOG_DONE_PIN, LOW); + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, LOW); delay(1); feed(); return true; } -void SolarExWatchdog::loop() { +void SolarExternalWatchdog::loop() { if (millis() > next_feed_watchdog) { feed(); - next_feed_watchdog = millis() + EX_WATCHDOG_TIMEOUT_MS; + next_feed_watchdog = millis() + EXTERNAL_WATCHDOG_TIMEOUT_MS; } } -unsigned long SolarExWatchdog::getIntervalMs() const { +unsigned long SolarExternalWatchdog::getIntervalMs() const { unsigned long interval_ms = 0; interval_ms = next_feed_watchdog - millis(); - if(interval_ms > EX_WATCHDOG_TIMEOUT_MS) { - interval_ms = EX_WATCHDOG_TIMEOUT_MS; + if(interval_ms > EXTERNAL_WATCHDOG_TIMEOUT_MS) { + interval_ms = EXTERNAL_WATCHDOG_TIMEOUT_MS; } return interval_ms; } -void SolarExWatchdog::feed() { - digitalWrite(EX_WATCHDOG_DONE_PIN, HIGH); +void SolarExternalWatchdog::feed() { + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, HIGH); delay(1); - digitalWrite(EX_WATCHDOG_DONE_PIN, LOW); + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, LOW); } diff --git a/variants/heltec_mesh_solar/target.h b/variants/heltec_mesh_solar/target.h index 3fc786726e..124e3afb3e 100644 --- a/variants/heltec_mesh_solar/target.h +++ b/variants/heltec_mesh_solar/target.h @@ -8,7 +8,7 @@ #include #include #include -#include +#include #ifdef DISPLAY_CLASS #include #endif @@ -31,9 +31,9 @@ class SolarSensorManager : public SensorManager { bool setSettingValue(const char* name, const char* value) override; }; -class SolarExWatchdog : public ExWatchdogManager { +class SolarExternalWatchdog : public ExternalWatchdogManager { public: - SolarExWatchdog() {} + SolarExternalWatchdog() {} bool begin() override; void loop() override; unsigned long getIntervalMs() const override; @@ -44,7 +44,7 @@ extern MeshSolarBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern SolarSensorManager sensors; -extern SolarExWatchdog ex_watchdog; +extern SolarExternalWatchdog external_watchdog; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; From 8435464c84fd86602f0b38bf77d33d7e065080e9 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 24 Mar 2026 10:48:15 +0800 Subject: [PATCH 04/74] Add v4 FEM LNA CLI control commands. --- docs/cli_commands.md | 14 +++++++++ examples/companion_radio/DataStore.cpp | 2 ++ examples/companion_radio/MyMesh.cpp | 31 +++++++++++++++++++- examples/companion_radio/NodePrefs.h | 3 +- examples/simple_repeater/MyMesh.cpp | 2 ++ examples/simple_room_server/MyMesh.cpp | 2 ++ examples/simple_sensor/SensorMesh.cpp | 2 ++ src/MeshCore.h | 5 +++- src/helpers/CommonCLI.cpp | 39 ++++++++++++++++++++++++-- src/helpers/CommonCLI.h | 1 + variants/heltec_v4/HeltecV4Board.cpp | 18 ++++++++++++ variants/heltec_v4/HeltecV4Board.h | 3 ++ variants/heltec_v4/LoRaFEMControl.h | 3 +- 13 files changed, 118 insertions(+), 7 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9769d71334..4dbf84e108 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -261,6 +261,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the LoRa FEM receive-path gain state on supported boards +**Usage:** +- `get radio.fem.rxgain` +- `set radio.fem.rxgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls the external LoRa FEM receive-path LNA where the board supports it. +- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode. + +--- + ### System #### View or change this node's name diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 40f1ceeb61..98a7a0dcb5 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -231,6 +231,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 90 file.close(); } @@ -269,6 +270,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 + file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 90 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b94e452674..d5d0abfed3 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,7 +46,9 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 -// NOTE: CMD range 44..49 parked, potentially for WiFi operations +#define CMD_GET_RADIO_FEM_RXGAIN 44 +#define CMD_SET_RADIO_FEM_RXGAIN 45 +// NOTE: CMD range 46..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 @@ -828,6 +830,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif + _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -866,6 +869,7 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _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 + _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -895,6 +899,7 @@ void MyMesh::begin(bool has_display) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1697,6 +1702,30 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } + } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else { + out_frame[0] = RESP_CODE_OK; + uint32_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; + memcpy(&out_frame[1], &value, 4); + _serial->writeFrame(out_frame, 5); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { + uint8_t value = cmd_frame[1]; + if (!board.canControlLoRaFemLna()) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else if (value <= 1) { + _prefs.radio_fem_rxgain = value; + if (board.setLoRaFemLnaEnabled(value != 0)) { + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 557be306c0..7ecfdf7da8 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,7 +29,8 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) + uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) -}; \ No newline at end of file +}; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..92affc5a8f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -896,6 +896,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_boosted_gain = 1; // enabled by default; #endif #endif + _prefs.radio_fem_rxgain = 1; pending_discover_tag = 0; pending_discover_until = 0; @@ -922,6 +923,7 @@ 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); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 7b94377387..7f61c26685 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -631,6 +631,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; next_post_idx = 0; next_client_idx = 0; @@ -649,6 +650,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 57d23a311e..58490fd0f0 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -729,6 +729,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; } void SensorMesh::begin(FILESYSTEM* fs) { @@ -741,6 +742,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/MeshCore.h b/src/MeshCore.h index 70cd0f0672..91b99c80ed 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -57,6 +57,9 @@ class MainBoard { 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; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } @@ -100,4 +103,4 @@ class RTCClock { } }; -} \ No newline at end of file +} diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 8b097c29fe..1c2afd371a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -88,7 +88,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -118,6 +119,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean file.close(); } @@ -179,7 +181,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 291 + // next: 292 file.close(); } @@ -327,6 +330,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif + } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported by this board"); + } else { + sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); + } } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); @@ -520,13 +529,37 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _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"); -#if defined(USE_SX1262) || defined(USE_SX1268) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { +#if defined(USE_SX1262) || defined(USE_SX1268) _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; strcpy(reply, "OK"); savePrefs(); _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); +#else + strcpy(reply, "Error: unsupported by this board"); #endif + } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { + if (!_board->canControlLoRaFemLna()) { + strcpy(reply, "Error: unsupported by this board"); + } 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 ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3a4332d1f2..82d5fcda57 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -58,6 +58,7 @@ struct NodePrefs { // persisted to file float adc_multiplier; char owner_info[120]; uint8_t rx_boosted_gain; // power settings + uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; }; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 49580d2ecf..4c79825a75 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -83,3 +83,21 @@ void HeltecV4Board::begin() { return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED"; #endif } + + bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecV4Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecV4Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 4d5ee46155..fe77caed05 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -19,5 +19,8 @@ class HeltecV4Board : public ESP32Board { 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/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index 7545296503..d84ebe9c6a 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -18,8 +18,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; From 65752fef72191293d8c548d6ba6826663fe947b6 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 24 Mar 2026 13:57:11 +0800 Subject: [PATCH 05/74] Fix the memory leak issue in the strdup function. --- examples/simple_repeater/UITask.cpp | 3 ++- examples/simple_room_server/UITask.cpp | 3 ++- examples/simple_sensor/UITask.cpp | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index d096d14b22..d1eae208e5 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -37,7 +37,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index 46311c5eb9..a48cc6b306 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -37,7 +37,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp index 0694bc3c1a..e16c826609 100644 --- a/examples/simple_sensor/UITask.cpp +++ b/examples/simple_sensor/UITask.cpp @@ -37,7 +37,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { From 2442e9a5bd97fe29830164dd57208b98212d442e Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 24 Mar 2026 16:05:28 +0800 Subject: [PATCH 06/74] Adapt LNA CLI control commands for heltec_tracker_v2. --- .../heltec_tracker_v2/HeltecTrackerV2Board.cpp | 18 ++++++++++++++++++ .../heltec_tracker_v2/HeltecTrackerV2Board.h | 3 +++ variants/heltec_tracker_v2/LoRaFEMControl.h | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index aabfed7967..f182c905e6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -82,3 +82,21 @@ void HeltecTrackerV2Board::begin() { const char* HeltecTrackerV2Board::getManufacturerName() const { return "Heltec Tracker V2"; } + + bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; + } + + bool HeltecTrackerV2Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); + } + + bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); + } diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 33c897bc94..ccbecc7ab6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -21,5 +21,8 @@ class HeltecTrackerV2Board : public ESP32Board { 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_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index 2c50b74289..0ce60fffd8 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -12,8 +12,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: bool lna_enabled = false; From 9664305a872e5d7f52cb4f967dd35935a7423312 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 24 Mar 2026 16:13:13 +0800 Subject: [PATCH 07/74] Adapt LNA CLI control commands for heltec_t096. --- variants/heltec_t096/LoRaFEMControl.h | 3 ++- variants/heltec_t096/T096Board.cpp | 20 +++++++++++++++++++- variants/heltec_t096/T096Board.h | 3 +++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index 2c50b74289..0ce60fffd8 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -12,8 +12,9 @@ class LoRaFEMControl void setRxModeEnable(void); void setRxModeEnableWhenMCUSleep(void); void setLNAEnable(bool enabled); - bool isLnaCanControl(void) { return lna_can_control; } + bool isLnaCanControl(void) const { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + bool isLNAEnabled(void) const { return lna_enabled; } private: bool lna_enabled = false; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 550131571f..54425145c4 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -123,4 +123,22 @@ void T096Board::powerOff() { const char* T096Board::getManufacturerName() const { return "Heltec T096"; -} \ No newline at end of file +} + +bool T096Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool T096Board::canControlLoRaFemLna() const { + return loRaFEMControl.isLnaCanControl(); +} + +bool T096Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index d1e3bdfdee..15c7e68b5d 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,4 +25,7 @@ class T096Board : public NRF52BoardDCDC { 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; }; From 22f07b3e4871bfde1e8013b25155988694721a04 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 25 Apr 2026 15:33:59 +0800 Subject: [PATCH 08/74] Revert "Merge branch 'meshcore-dev:main' into cli-lna-command" This reverts commit ca047fe0c05315ed77e64cfd72ee7a17b810a929, reversing changes made to ddedb3c7a776f3f075d0bde2123b1c6f31186b90. --- README.md | 2 +- docs/faq.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8b9e5e083..ebad1f6f59 100644 --- a/README.md +++ b/README.md @@ -117,4 +117,4 @@ There are a number of fairly major features in the pipeline, with no particular - Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/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. +- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community. diff --git a/docs/faq.md b/docs/faq.md index 3edc0a6953..9fe1534a1a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -194,7 +194,7 @@ Recently, as of October 2025, many regions have moved to the "narrow" setting, a After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions. -If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://meshcore.gg) to let Liam Cottle know. +If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://discord.gg/cYtQNYCCRK) to let Liam Cottle know. @@ -526,7 +526,7 @@ The third character is the capital letter 'O', not zero `0` - Firmware repo: https://github.com/meshcore-dev/MeshCore ### 5.8. Q: How can I support MeshCore? -**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://meshcore.gg). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at . +**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at . Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase From c42f6db0ebf53f821c4161e54a444ddaa4dd22b4 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 25 Apr 2026 16:01:26 +0800 Subject: [PATCH 09/74] Revise according to the review comments. Co-authored-by: Copilot --- examples/companion_radio/MyMesh.cpp | 11 ++++++----- src/helpers/CommonCLI.cpp | 10 +++++----- variants/heltec_v4/HeltecV4Board.h | 1 - 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 91da9bccf9..e98a78733d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,8 +46,7 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 -#define CMD_GET_RADIO_FEM_RXGAIN 44 -#define CMD_SET_RADIO_FEM_RXGAIN 45 + // NOTE: CMD range 46..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 @@ -63,6 +62,8 @@ #define CMD_SEND_CHANNEL_DATA 62 #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 +#define CMD_GET_RADIO_FEM_RXGAIN 65 +#define CMD_SET_RADIO_FEM_RXGAIN 66 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -1808,9 +1809,9 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); } else { out_frame[0] = RESP_CODE_OK; - uint32_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; - memcpy(&out_frame[1], &value, 4); - _serial->writeFrame(out_frame, 5); + uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; + memcpy(&out_frame[1], &value, 1); + _serial->writeFrame(out_frame, 2); } } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { uint8_t value = cmd_frame[1]; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b9152f4cd2..d22e08a8fe 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -559,7 +559,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep #endif } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); } else if (memcmp(&config[17], "on", 2) == 0) { if (_board->setLoRaFemLnaEnabled(true)) { _prefs->radio_fem_rxgain = 1; @@ -750,7 +750,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } } else { _prefs->adc_multiplier = 0.0f; - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); }; } else { strcpy(reply, "unknown config: "); @@ -800,7 +800,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #endif } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); } @@ -885,12 +885,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "> unknown"); } #else - strcpy(reply, "ERROR: unsupported"); + strcpy(reply, "Error: unsupported"); #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { - strcpy(reply, "Error: unsupported by this board"); + strcpy(reply, "Error: unsupported"); } else { sprintf(reply, "> %.3f", adc_mult); } diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index b2caaa354c..fc37b9f6ae 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -25,7 +25,6 @@ class HeltecV4Board : public ESP32Board { void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; - const char* getManufacturerName() const override ; bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; bool isLoRaFemLnaEnabled() const override; From 9d26953398d75c1c57c5c818957b5384e86921e8 Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:10:57 +0800 Subject: [PATCH 10/74] Remove unnecessary blank line in MyMesh.cpp --- examples/companion_radio/MyMesh.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e98a78733d..1b664eeadc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,7 +46,6 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 - // NOTE: CMD range 46..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 From 62b0d82682ec9cf51c0e8f08aa835a6c8bf6aefc Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:12:40 +0800 Subject: [PATCH 11/74] Restore WiFi operation command scope comments --- 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 1b664eeadc..2d1f6dcd43 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -46,7 +46,7 @@ #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 #define CMD_GET_TUNING_PARAMS 43 -// NOTE: CMD range 46..49 parked, potentially for WiFi operations +// NOTE: CMD range 44..49 parked, potentially for WiFi operations #define CMD_SEND_BINARY_REQ 50 #define CMD_FACTORY_RESET 51 #define CMD_SEND_PATH_DISCOVERY_REQ 52 From 444dcfb8fd564fb10a131aa75415d41f7f66107c Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:06:21 +0800 Subject: [PATCH 12/74] Change write to read for radio_fem_rxgain --- examples/companion_radio/DataStore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index f1dc678eac..362ba68a33 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,7 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 + file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } From 363ac44461a9299c9a43641598c18ba65fb6ffea Mon Sep 17 00:00:00 2001 From: Quency-D Date: Mon, 27 Apr 2026 15:32:02 +0800 Subject: [PATCH 13/74] Fix external watchdog timing around sleep --- examples/simple_repeater/main.cpp | 11 ++++++----- src/helpers/ExternalWatchdogManager.h | 4 ++-- variants/heltec_mesh_solar/platformio.ini | 4 ++-- variants/heltec_mesh_solar/target.cpp | 15 +++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index f636c32abd..c4e318d512 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -164,16 +164,17 @@ void loop() { if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) #ifdef HAS_EXTERNAL_WATCHDOG - uint32_t sleep_interval = external_watchdog.getIntervalMs()/1000; - board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet + external_watchdog.feed(); + uint32_t sleep_interval = external_watchdog.getIntervalMs() / 1000; + board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // nrf ignores seconds param, sleeps whenever possible #else - board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet -#endif board.sleep(1800); // nrf ignores seconds param, sleeps whenever possible +#endif #else if (the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep #ifdef HAS_EXTERNAL_WATCHDOG - uint32_t sleep_interval = external_watchdog.getIntervalMs()/1000; + external_watchdog.feed(); + uint32_t sleep_interval = external_watchdog.getIntervalMs() / 1000; board.sleep((sleep_interval > 1800) ? 1800 : sleep_interval); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet #else board.sleep(1800); // To sleep. Wake up after 30 minutes or when receiving a LoRa packet diff --git a/src/helpers/ExternalWatchdogManager.h b/src/helpers/ExternalWatchdogManager.h index 9ef8abaa61..cb4f492360 100644 --- a/src/helpers/ExternalWatchdogManager.h +++ b/src/helpers/ExternalWatchdogManager.h @@ -2,9 +2,9 @@ class ExternalWatchdogManager { protected: - unsigned long next_feed_watchdog; + unsigned long last_feed_watchdog; public: - ExternalWatchdogManager() { next_feed_watchdog = 0; } + ExternalWatchdogManager() { last_feed_watchdog = 0; } virtual bool begin() { return false; } virtual void loop() { } virtual unsigned long getIntervalMs() const { return 0; } diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index 1bc7e7fa7a..38594ecc2c 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -17,7 +17,7 @@ build_flags = ${nrf52_base.build_flags} -D HAS_EXTERNAL_WATCHDOG -D EXTERNAL_WATCHDOG_DONE_PIN=9 -D EXTERNAL_WATCHDOG_WAKE_PIN=10 - -D EXTERNAL_WATCHDOG_TIMEOUT_MS=480000 ;(6*60*1000) ; 6 minute watchdog + -D EXTERNAL_WATCHDOG_FEED_INTERVAL_MS=480000 ; 8 minute feed interval, safely inside the hardware watchdog timeout build_src_filter = ${nrf52_base.build_src_filter} + @@ -97,4 +97,4 @@ build_src_filter = ${Heltec_mesh_solar.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_mesh_solar.lib_deps} - densaugeo/base64 @ ~1.4.0 \ No newline at end of file + densaugeo/base64 @ ~1.4.0 diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index a40b8ce999..96411c55f3 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -124,7 +124,7 @@ bool SolarSensorManager::setSettingValue(const char* name, const char* value) { } bool SolarExternalWatchdog::begin() { - next_feed_watchdog = 0; + last_feed_watchdog = 0; pinMode(EXTERNAL_WATCHDOG_WAKE_PIN, INPUT); pinMode(EXTERNAL_WATCHDOG_DONE_PIN, OUTPUT); delay(1); @@ -134,23 +134,22 @@ bool SolarExternalWatchdog::begin() { return true; } void SolarExternalWatchdog::loop() { - if (millis() > next_feed_watchdog) { + if (millis() - last_feed_watchdog >= EXTERNAL_WATCHDOG_FEED_INTERVAL_MS) { feed(); - next_feed_watchdog = millis() + EXTERNAL_WATCHDOG_TIMEOUT_MS; } } unsigned long SolarExternalWatchdog::getIntervalMs() const { - unsigned long interval_ms = 0; - interval_ms = next_feed_watchdog - millis(); - if(interval_ms > EXTERNAL_WATCHDOG_TIMEOUT_MS) { - interval_ms = EXTERNAL_WATCHDOG_TIMEOUT_MS; + unsigned long elapsed_ms = millis() - last_feed_watchdog; + if (elapsed_ms >= EXTERNAL_WATCHDOG_FEED_INTERVAL_MS) { + return 0; } - return interval_ms; + return EXTERNAL_WATCHDOG_FEED_INTERVAL_MS - elapsed_ms; } void SolarExternalWatchdog::feed() { digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, HIGH); delay(1); digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, LOW); + last_feed_watchdog = millis(); } From 8e787f6ee401e7671446bf05ce4839b614656252 Mon Sep 17 00:00:00 2001 From: jirogit Date: Tue, 21 Apr 2026 22:23:42 -0700 Subject: [PATCH 14/74] fix(RegionMap): save() returns actual write success instead of hardcoded true --- src/helpers/RegionMap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 7b8399e260..36c1d19d59 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -139,7 +139,7 @@ bool RegionMap::save(FILESYSTEM* _fs, const char* path) { } } file.close(); - return true; + return success; } return false; // failed } From 3a546a6395c440a2c463283ef1b3e7d178e81836 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 12 May 2026 14:53:17 +0800 Subject: [PATCH 15/74] add heltec tower v2 --- boards/heltec_tower_v2.json | 61 +++++++++++ .../heltec_tower_v2/HeltecTowerV2Board.cpp | 102 +++++++++++++++++ variants/heltec_tower_v2/HeltecTowerV2Board.h | 23 ++++ variants/heltec_tower_v2/platformio.ini | 103 ++++++++++++++++++ variants/heltec_tower_v2/target.cpp | 31 ++++++ variants/heltec_tower_v2/target.h | 28 +++++ variants/heltec_tower_v2/variant.cpp | 25 +++++ variants/heltec_tower_v2/variant.h | 101 +++++++++++++++++ 8 files changed, 474 insertions(+) create mode 100644 boards/heltec_tower_v2.json create mode 100644 variants/heltec_tower_v2/HeltecTowerV2Board.cpp create mode 100644 variants/heltec_tower_v2/HeltecTowerV2Board.h create mode 100644 variants/heltec_tower_v2/platformio.ini create mode 100644 variants/heltec_tower_v2/target.cpp create mode 100644 variants/heltec_tower_v2/target.h create mode 100644 variants/heltec_tower_v2/variant.cpp create mode 100644 variants/heltec_tower_v2/variant.h diff --git a/boards/heltec_tower_v2.json b/boards/heltec_tower_v2.json new file mode 100644 index 0000000000..9f9b6ab591 --- /dev/null +++ b/boards/heltec_tower_v2.json @@ -0,0 +1,61 @@ +{ + "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"], + ["0x239A","0x0071"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_tower_v2", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Heltec Tower V2 Board", + "upload": { + "maximum_ram_size": 235520, + "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": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp new file mode 100644 index 0000000000..2fd7a61823 --- /dev/null +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp @@ -0,0 +1,102 @@ +#include "HeltecTowerV2Board.h" + +#include +#include + +extern void variant_shutdown(); + +#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 HeltecTowerV2Board::initiateShutdown(uint8_t reason) { + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE); + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); + + bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT); + pinMode(PIN_BAT_CTL, OUTPUT); + digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW); + + if (enable_lpcomp) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + variant_shutdown(); + enterSystemOff(reason); +} +#endif + +void HeltecTowerV2Board::begin() { + NRF52Board::begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); +#endif + + pinMode(PIN_BAT_CTL, OUTPUT); + digitalWrite(PIN_BAT_CTL, LOW); + +#ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); +#endif + + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); + Wire.begin(); + + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); +} + +#ifdef P_LORA_TX_LED +void HeltecTowerV2Board::onBeforeTransmit() { + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); +} + +void HeltecTowerV2Board::onAfterTransmit() { + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); +} +#endif + +uint16_t HeltecTowerV2Board::getBattMilliVolts() { + analogReadResolution(12); + analogReference(VBAT_AR_INTERNAL); + pinMode(PIN_VBAT_READ, INPUT); + pinMode(PIN_BAT_CTL, OUTPUT); + digitalWrite(PIN_BAT_CTL, HIGH); + + delay(10); + int adcvalue = analogRead(PIN_VBAT_READ); + digitalWrite(PIN_BAT_CTL, LOW); + + return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER); +} + +const char* HeltecTowerV2Board::getManufacturerName() const { + return "Heltec Tower V2"; +} + +void HeltecTowerV2Board::powerOff() { + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE); + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); + pinMode(PIN_BAT_CTL, OUTPUT); + digitalWrite(PIN_BAT_CTL, LOW); + variant_shutdown(); + sd_power_system_off(); +} diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.h b/variants/heltec_tower_v2/HeltecTowerV2Board.h new file mode 100644 index 0000000000..817d3c1a44 --- /dev/null +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +class HeltecTowerV2Board : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + HeltecTowerV2Board() : NRF52Board("TOWER_V2_OTA") {} + void begin(); +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override; + void onAfterTransmit() override; +#endif + uint16_t getBattMilliVolts() override; + const char* getManufacturerName() const override; + void powerOff() override; +}; diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini new file mode 100644 index 0000000000..2fac9fbc3e --- /dev/null +++ b/variants/heltec_tower_v2/platformio.ini @@ -0,0 +1,103 @@ +[Heltec_tower_v2] +extends = nrf52_base +board = heltec_tower_v2 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -D ENV_INCLUDE_GPS=1 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/heltec_tower_v2 + -D HELTEC_TOWER_V2 + -D NRF52_POWER_MANAGEMENT + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/heltec_tower_v2> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 +debug_tool = jlink +upload_protocol = nrfutil + +[env:Heltec_tower_v2_repeater] +extends = Heltec_tower_v2 +build_src_filter = ${Heltec_tower_v2.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${Heltec_tower_v2.build_flags} + -D ADVERT_NAME='"Heltec_Tower_V2 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 + +[env:Heltec_tower_v2_room_server] +extends = Heltec_tower_v2 +build_src_filter = ${Heltec_tower_v2.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${Heltec_tower_v2.build_flags} + -D ADVERT_NAME='"Heltec_Tower_V2 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 + +[env:Heltec_tower_v2_companion_radio_ble] +extends = Heltec_tower_v2 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${Heltec_tower_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -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 MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tower_v2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_tower_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Heltec_tower_v2_companion_radio_usb] +extends = Heltec_tower_v2 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${Heltec_tower_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tower_v2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_tower_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Heltec_tower_v2_kiss_modem] +extends = Heltec_tower_v2 +build_src_filter = ${Heltec_tower_v2.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_tower_v2/target.cpp b/variants/heltec_tower_v2/target.cpp new file mode 100644 index 0000000000..ad45735487 --- /dev/null +++ b/variants/heltec_tower_v2/target.cpp @@ -0,0 +1,31 @@ +#include "target.h" + +#include +#include +#include + +HeltecTowerV2Board 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); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, 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); +} diff --git a/variants/heltec_tower_v2/target.h b/variants/heltec_tower_v2/target.h new file mode 100644 index 0000000000..0371924614 --- /dev/null +++ b/variants/heltec_tower_v2/target.h @@ -0,0 +1,28 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS +#include +#include "helpers/ui/NullDisplayDriver.h" +#endif + +extern HeltecTowerV2Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS +extern DISPLAY_CLASS display; +extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/heltec_tower_v2/variant.cpp b/variants/heltec_tower_v2/variant.cpp new file mode 100644 index 0000000000..699c6e4af2 --- /dev/null +++ b/variants/heltec_tower_v2/variant.cpp @@ -0,0 +1,25 @@ +#include "variant.h" + +#include "Arduino.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +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() +{ + +} + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} diff --git a/variants/heltec_tower_v2/variant.h b/variants/heltec_tower_v2/variant.h new file mode 100644 index 0000000000..d3f2599a0f --- /dev/null +++ b/variants/heltec_tower_v2/variant.h @@ -0,0 +1,101 @@ +#pragma once + +#include "WVariant.h" + +#define USE_LFXO +#define VARIANT_MCK (64000000ul) + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_WIRE_SDA (0 + 11) +#define PIN_WIRE_SCL (0 + 12) +#define PIN_BOARD_SDA PIN_WIRE_SDA +#define PIN_BOARD_SCL PIN_WIRE_SCL + +#define SPI_INTERFACES_COUNT (1) +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) +#define PIN_SPI_NSS LORA_CS + +#define LED_BUILTIN (32 + 15) +#define PIN_LED LED_BUILTIN +#define LED_RED (-1) +#define LED_GREEN (-1) +#define LED_BLUE (-1) +#define LED_PIN (-1) +#define P_LORA_TX_LED LED_BUILTIN +#define LED_STATE_ON LOW + +#define PIN_BUTTON1 (32 + 10) +#define BUTTON_PIN PIN_BUTTON1 +#define PIN_USER_BTN BUTTON_PIN + +#define USE_SX1262 +#define SX126X_CS (0 + 24) +#define LORA_CS SX126X_CS +#define SX126X_DIO1 (0 + 20) +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define P_LORA_NSS LORA_CS +#define P_LORA_DIO_1 SX126X_DIO1 +#define P_LORA_BUSY SX126X_BUSY +#define P_LORA_RESET SX126X_RESET +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_SCLK PIN_SPI_SCK + +#define GPS_L76K +#define GPS_RESET_MODE LOW +#define PIN_GPS_RESET (32 + 6) +#define PIN_GPS_RESET_ACTIVE GPS_RESET_MODE +#define PIN_GPS_EN (0 + 7) +#define PIN_GPS_EN_ACTIVE LOW +#define GPS_EN_ACTIVE PIN_GPS_EN_ACTIVE +#define PIN_GPS_STANDBY (32 + 2) +#define PIN_GPS_PPS (32 + 4) +#define GPS_BAUD_RATE 9600 + +// Upstream names are from the GPS perspective. MeshCore's PIN_GPS_TX is the +// CPU RX pin because EnvironmentSensorManager passes it as Serial1 RX. +#define GPS_TX_PIN (32 + 7) +#define GPS_RX_PIN (32 + 5) +#define PIN_GPS_TX GPS_RX_PIN +#define PIN_GPS_RX GPS_TX_PIN + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +#define HAS_HARDWARE_WATCHDOG +#define HARDWARE_WATCHDOG_DONE (0 + 9) +#define HARDWARE_WATCHDOG_WAKE (0 + 10) +#define HARDWARE_WATCHDOG_TIMEOUT_MS (8 * 60 * 1000) + +#define SERIAL_PRINT_PORT 0 + +#define PIN_BAT_CTL (0 + 21) +#define ADC_CTRL PIN_BAT_CTL +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN (0 + 4) +#define PIN_VBAT_READ BATTERY_PIN +#define ADC_RESOLUTION 14 +#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 (4.916F) +#define MV_LSB (3000.0F / 4096.0F) + +#define NRF52_POWER_MANAGEMENT +#define PWRMGT_VOLTAGE_BOOTLOCK 3100 +#define PWRMGT_LPCOMP_AIN 2 +#define PWRMGT_LPCOMP_REFSEL 1 \ No newline at end of file From f29cae602f0f2e1102fb085023202ad6861620e1 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 16 May 2026 09:30:49 +0700 Subject: [PATCH 16/74] Added flash_mode=dio to avoid boot loop when flashing using merge.bin --- variants/xiao_c6/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 9f504b8ed0..0d8c2a79d3 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -1,6 +1,7 @@ [Xiao_C6] extends = esp32c6_base board = esp32-c6-devkitm-1 +board_build.flash_mode = dio board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} From 00d445a269451e2af0bd54365346c9a8b584f300 Mon Sep 17 00:00:00 2001 From: Sefinek Date: Thu, 28 May 2026 15:44:56 +0200 Subject: [PATCH 17/74] fix: fix typos in source code comments --- examples/companion_radio/DataStore.cpp | 2 +- src/Mesh.h | 2 +- src/Packet.h | 2 +- src/helpers/bridges/ESPNowBridge.cpp | 2 +- src/helpers/stm32/InternalFileSystem.cpp | 6 +++--- src/helpers/ui/OLEDDisplay.cpp | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c7988bb344..dda0a84ec9 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -541,7 +541,7 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src uint32_t pos = 0, found_pos = 0; uint32_t min_timestamp = 0xFFFFFFFF; - // search for matching key OR evict by oldest timestmap + // search for matching key OR evict by oldest timestamp BlobRec tmp; file.seek(0); while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { diff --git a/src/Mesh.h b/src/Mesh.h index d53d6d25fa..d3c1f73ce1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -100,7 +100,7 @@ class Mesh : public Dispatcher { * \param auth_code a code to authenticate the packet * \param flags zero for now * \param path_snrs single byte SNR*4 for each hop in the path - * \param path_hashes hashes if each repeater in the path + * \param path_hashes hashes of each repeater in the path * \param path_len length of the path_snrs[] and path_hashes[] arrays */ virtual void onTraceRecv(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) { } diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..c19d9e9d8f 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -25,7 +25,7 @@ namespace mesh { #define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type(uint16), data_len, blob) #define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) #define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) -#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop +#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNR for each hop #define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets #define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet //... diff --git a/src/helpers/bridges/ESPNowBridge.cpp b/src/helpers/bridges/ESPNowBridge.cpp index b9eb1c105c..808e9df434 100644 --- a/src/helpers/bridges/ESPNowBridge.cpp +++ b/src/helpers/bridges/ESPNowBridge.cpp @@ -32,7 +32,7 @@ void ESPNowBridge::begin() { // Initialize WiFi in station mode WiFi.mode(WIFI_STA); - // Set wifi channel + // Set Wi-Fi channel if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) { BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel); return; diff --git a/src/helpers/stm32/InternalFileSystem.cpp b/src/helpers/stm32/InternalFileSystem.cpp index dc032eb968..6a7d7064ed 100644 --- a/src/helpers/stm32/InternalFileSystem.cpp +++ b/src/helpers/stm32/InternalFileSystem.cpp @@ -37,7 +37,7 @@ static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, l } // Program a region in a block. The block must have previously -// been erased. Negative error codes are propogated to the user. +// been erased. Negative error codes are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { @@ -62,7 +62,7 @@ static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, l // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes -// are propogated to the user. +// are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) { @@ -87,7 +87,7 @@ static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) } // Sync the state of the underlying block device. Negative error codes -// are propogated to the user. +// are propagated to the user. static int _internal_flash_sync(const struct lfs_config *c) { return LFS_ERR_OK; // don't need sync diff --git a/src/helpers/ui/OLEDDisplay.cpp b/src/helpers/ui/OLEDDisplay.cpp index 1910134400..aa11ce1a82 100644 --- a/src/helpers/ui/OLEDDisplay.cpp +++ b/src/helpers/ui/OLEDDisplay.cpp @@ -1155,7 +1155,7 @@ void OLEDDisplay::setFontTableLookupFunction(FontTableLookupFunction function) { char DefaultFontTableLookup(const uint8_t ch) { // UTF-8 to font table index converter - // Code form http://playground.arduino.cc/Main/Utf8ascii + // Code from http://playground.arduino.cc/Main/Utf8ascii static uint8_t LASTCHAR; if (ch < 128) { // Standard ASCII-set 0..0x7F handling @@ -1166,7 +1166,7 @@ char DefaultFontTableLookup(const uint8_t ch) { uint8_t last = LASTCHAR; // get last char LASTCHAR = ch; - switch (last) { // conversion depnding on first UTF8-character + switch (last) { // conversion depending on first UTF8-character case 0xC2: return (uint8_t) ch; case 0xC3: return (uint8_t) (ch | 0xC0); case 0x82: if (ch == 0xAC) return (uint8_t) 0x80; // special case Euro-symbol From 4bf391f5c38f4d65cc595e8ba747e1a2e5cebaa6 Mon Sep 17 00:00:00 2001 From: Sefinek Date: Thu, 28 May 2026 16:38:39 +0200 Subject: [PATCH 18/74] fix: fix typos in source code comments --- src/Mesh.h | 4 ++-- src/helpers/sensors/LPPDataHelpers.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index d3c1f73ce1..a4967c1d72 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -212,12 +212,12 @@ class Mesh : public Dispatcher { void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops) + * \brief send a locally-generated Packet to just neighbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes + * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 37b50f3f12..70a036c493 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -142,7 +142,7 @@ class LPPReader { case LPP_GPS: _pos += 9; break; case LPP_POLYLINE: - _pos += 8; break; // TODO: this is MINIMIUM + _pos += 8; break; // TODO: this is MINIMUM case LPP_GYROMETER: case LPP_ACCELEROMETER: _pos += 6; break; From 07bfe9069565729205d79edf4cd2a0071b22fe3f Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 9 Jun 2026 00:31:48 +1200 Subject: [PATCH 19/74] free packet on parse failure --- examples/companion_radio/MyMesh.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6fbb0f7428..0735398864 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1981,6 +1981,7 @@ void MyMesh::handleCmdFrame(size_t len) { sendPacket(pkt, priority, 0); writeOKFrame(); } else { + _mgr->free(pkt); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } } else { From ae0bb7ee9544db0a0eb05ce7405432cb8ed6496a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 9 Jun 2026 00:42:58 +1200 Subject: [PATCH 20/74] use releasePacket instead of _mgr->free --- 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 0735398864..c468967f97 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1981,7 +1981,7 @@ void MyMesh::handleCmdFrame(size_t len) { sendPacket(pkt, priority, 0); writeOKFrame(); } else { - _mgr->free(pkt); + releasePacket(pkt); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } } else { From fe56d01da28b5bf552274cf946a147ffb558a876 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 12 Jun 2026 17:58:19 +0800 Subject: [PATCH 21/74] add tower v2 PA control --- .../heltec_tower_v2/HeltecTowerV2Board.cpp | 9 +++-- variants/heltec_tower_v2/HeltecTowerV2Board.h | 5 ++- variants/heltec_tower_v2/LoRaFEMControl.cpp | 40 +++++++++++++++++++ variants/heltec_tower_v2/LoRaFEMControl.h | 13 ++++++ variants/heltec_tower_v2/platformio.ini | 2 +- variants/heltec_tower_v2/variant.cpp | 15 +++++++ variants/heltec_tower_v2/variant.h | 13 ++++-- 7 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 variants/heltec_tower_v2/LoRaFEMControl.cpp create mode 100644 variants/heltec_tower_v2/LoRaFEMControl.h diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp index 2fd7a61823..04213bda8c 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp @@ -19,6 +19,7 @@ void HeltecTowerV2Board::initiateShutdown(uint8_t reason) { digitalWrite(PIN_GPS_STANDBY, LOW); pinMode(PIN_GPS_RESET, OUTPUT); digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); + loRaFEMControl.setSleepModeEnable(); bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT); @@ -37,10 +38,8 @@ void HeltecTowerV2Board::initiateShutdown(uint8_t reason) { void HeltecTowerV2Board::begin() { NRF52Board::begin(); -#ifdef P_LORA_TX_LED pinMode(P_LORA_TX_LED, OUTPUT); digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); -#endif pinMode(PIN_BAT_CTL, OUTPUT); digitalWrite(PIN_BAT_CTL, LOW); @@ -58,17 +57,18 @@ void HeltecTowerV2Board::begin() { digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); pinMode(PIN_GPS_STANDBY, OUTPUT); digitalWrite(PIN_GPS_STANDBY, HIGH); + loRaFEMControl.init(); } -#ifdef P_LORA_TX_LED void HeltecTowerV2Board::onBeforeTransmit() { digitalWrite(P_LORA_TX_LED, LED_STATE_ON); + loRaFEMControl.setTxModeEnable(); } void HeltecTowerV2Board::onAfterTransmit() { digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); + loRaFEMControl.setRxModeEnable(); } -#endif uint16_t HeltecTowerV2Board::getBattMilliVolts() { analogReadResolution(12); @@ -95,6 +95,7 @@ void HeltecTowerV2Board::powerOff() { digitalWrite(PIN_GPS_STANDBY, LOW); pinMode(PIN_GPS_RESET, OUTPUT); digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); + loRaFEMControl.setSleepModeEnable(); pinMode(PIN_BAT_CTL, OUTPUT); digitalWrite(PIN_BAT_CTL, LOW); variant_shutdown(); diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.h b/variants/heltec_tower_v2/HeltecTowerV2Board.h index 817d3c1a44..6912acdec7 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.h +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.h @@ -3,6 +3,7 @@ #include #include #include +#include "LoRaFEMControl.h" class HeltecTowerV2Board : public NRF52BoardDCDC { protected: @@ -11,12 +12,12 @@ class HeltecTowerV2Board : public NRF52BoardDCDC { #endif public: + LoRaFEMControl loRaFEMControl; + HeltecTowerV2Board() : NRF52Board("TOWER_V2_OTA") {} void begin(); -#ifdef P_LORA_TX_LED void onBeforeTransmit() override; void onAfterTransmit() override; -#endif uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; diff --git a/variants/heltec_tower_v2/LoRaFEMControl.cpp b/variants/heltec_tower_v2/LoRaFEMControl.cpp new file mode 100644 index 0000000000..d87d1849eb --- /dev/null +++ b/variants/heltec_tower_v2/LoRaFEMControl.cpp @@ -0,0 +1,40 @@ +#include "LoRaFEMControl.h" + +#include +#include "variant.h" + +static void enableFEMPower() { + bool wasOff = digitalRead(LORA_KCT8103L_EN) != HIGH; + digitalWrite(LORA_KCT8103L_EN, HIGH); + if (wasOff) { + delay(5); + } +} + +void LoRaFEMControl::init() { + pinMode(LORA_KCT8103L_EN, OUTPUT); + digitalWrite(LORA_KCT8103L_EN, HIGH); + delay(1); + pinMode(LORA_KCT8103L_TX_RX, OUTPUT); + digitalWrite(LORA_KCT8103L_TX_RX, LOW); +} + +void LoRaFEMControl::setSleepModeEnable() { + pinMode(LORA_KCT8103L_EN, OUTPUT); + digitalWrite(LORA_KCT8103L_EN, LOW); +} + +void LoRaFEMControl::setTxModeEnable() { + enableFEMPower(); + digitalWrite(LORA_KCT8103L_TX_RX, HIGH); +} + +void LoRaFEMControl::setRxModeEnable() { + enableFEMPower(); + digitalWrite(LORA_KCT8103L_TX_RX, LOW); +} + +void LoRaFEMControl::setRxModeEnableWhenMCUSleep() { + enableFEMPower(); + digitalWrite(LORA_KCT8103L_TX_RX, LOW); +} diff --git a/variants/heltec_tower_v2/LoRaFEMControl.h b/variants/heltec_tower_v2/LoRaFEMControl.h new file mode 100644 index 0000000000..e6c0ad2f11 --- /dev/null +++ b/variants/heltec_tower_v2/LoRaFEMControl.h @@ -0,0 +1,13 @@ +#pragma once + +class LoRaFEMControl { +public: + LoRaFEMControl() {} + virtual ~LoRaFEMControl() {} + + void init(); + void setSleepModeEnable(); + void setTxModeEnable(); + void setRxModeEnable(); + void setRxModeEnableWhenMCUSleep(); +}; diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index 2fac9fbc3e..190a21c906 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -11,7 +11,7 @@ build_flags = ${nrf52_base.build_flags} -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 + -D LORA_TX_POWER=12 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/heltec_tower_v2/variant.cpp b/variants/heltec_tower_v2/variant.cpp index 699c6e4af2..66ec2111c1 100644 --- a/variants/heltec_tower_v2/variant.cpp +++ b/variants/heltec_tower_v2/variant.cpp @@ -19,7 +19,22 @@ void initVariant() void variant_shutdown() { + nrf_gpio_cfg_default(PIN_GPS_EN); nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(PIN_GPS_STANDBY); + nrf_gpio_cfg_default(GPS_RX_PIN); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(LORA_KCT8103L_TX_RX); + nrf_gpio_cfg_default(RF_PA_DETECT_PIN); + nrf_gpio_cfg_default(SX126X_CS); + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + nrf_gpio_cfg_default(PIN_LED); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); } diff --git a/variants/heltec_tower_v2/variant.h b/variants/heltec_tower_v2/variant.h index d3f2599a0f..352184b866 100644 --- a/variants/heltec_tower_v2/variant.h +++ b/variants/heltec_tower_v2/variant.h @@ -11,8 +11,8 @@ #define NUM_ANALOG_OUTPUTS (0) #define WIRE_INTERFACES_COUNT (1) -#define PIN_WIRE_SDA (0 + 11) -#define PIN_WIRE_SCL (0 + 12) +#define PIN_WIRE_SDA (0 + 30) +#define PIN_WIRE_SCL (0 + 5) #define PIN_BOARD_SDA PIN_WIRE_SDA #define PIN_BOARD_SCL PIN_WIRE_SCL @@ -52,6 +52,13 @@ #define P_LORA_MOSI PIN_SPI_MOSI #define P_LORA_SCLK PIN_SPI_SCK +#define USE_KCT8103L_PA_ONLY +#define LORA_KCT8103L_EN (0 + 15) +#define LORA_KCT8103L_TX_RX (0 + 16) +#define LORA_PA_POWER LORA_KCT8103L_EN +#define RF_PA_DETECT_PIN (0 + 13) +#define RF_PA_HIGH_POWER_VALUE HIGH + #define GPS_L76K #define GPS_RESET_MODE LOW #define PIN_GPS_RESET (32 + 6) @@ -98,4 +105,4 @@ #define NRF52_POWER_MANAGEMENT #define PWRMGT_VOLTAGE_BOOTLOCK 3100 #define PWRMGT_LPCOMP_AIN 2 -#define PWRMGT_LPCOMP_REFSEL 1 \ No newline at end of file +#define PWRMGT_LPCOMP_REFSEL 1 From d2d3d44c9be7f2f6e90f191a7cb7a3c1d361fc75 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 12 Jun 2026 18:05:27 +0800 Subject: [PATCH 22/74] Add maximum power limit --- variants/heltec_tower_v2/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index 190a21c906..f029b7d41f 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -12,6 +12,7 @@ build_flags = ${nrf52_base.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=12 + -D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~29dBm at antenna -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52_base.build_src_filter} From 1c4c995a41777781c390ff3a41f8a1c6eb221ee1 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 13 Jun 2026 16:00:06 +0800 Subject: [PATCH 23/74] Fix low power consumption issues --- variants/heltec_tower_v2/variant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_tower_v2/variant.cpp b/variants/heltec_tower_v2/variant.cpp index 66ec2111c1..cfb88c4a3e 100644 --- a/variants/heltec_tower_v2/variant.cpp +++ b/variants/heltec_tower_v2/variant.cpp @@ -25,6 +25,8 @@ void variant_shutdown() nrf_gpio_cfg_default(PIN_GPS_STANDBY); nrf_gpio_cfg_default(GPS_RX_PIN); nrf_gpio_cfg_default(GPS_TX_PIN); + pinMode(LORA_KCT8103L_EN, OUTPUT); + digitalWrite(LORA_KCT8103L_EN, LOW); nrf_gpio_cfg_default(LORA_KCT8103L_TX_RX); nrf_gpio_cfg_default(RF_PA_DETECT_PIN); nrf_gpio_cfg_default(SX126X_CS); From d5f74e93c530a41a2c88dbcc5f34f06860129179 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 13 Jun 2026 18:19:41 +1000 Subject: [PATCH 24/74] * PAYLOAD_TYPE_PATH bad path_len now rejected --- src/Mesh.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 87ad61af2e..e9b92262ce 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -155,6 +155,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { int k = 0; uint8_t path_len = data[k++]; + if (!Packet::isValidPathLen(path_len)) { + MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, bad path_len: %u", getLogDateTime(), (uint32_t)path_len); + break; // reject bad encoding + } uint8_t hash_size = (path_len >> 6) + 1; uint8_t hash_count = path_len & 63; uint8_t* path = &data[k]; k += hash_size*hash_count; From 3ee58fd2e3f16cad205611503b6dd71728aac23c Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 13 Jun 2026 17:31:54 +0800 Subject: [PATCH 25/74] Remove companion FEM RX gain command IDs --- examples/companion_radio/DataStore.cpp | 2 -- examples/companion_radio/MyMesh.cpp | 29 -------------------------- examples/companion_radio/NodePrefs.h | 1 - 3 files changed, 32 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index fdb924ad4c..bf2f36c3d9 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,7 +233,6 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -274,7 +273,6 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1dd5162bf9..c468967f97 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,8 +62,6 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 -#define CMD_GET_RADIO_FEM_RXGAIN 66 -#define CMD_SET_RADIO_FEM_RXGAIN 67 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -888,7 +886,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif - _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -938,7 +935,6 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _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 - _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -968,7 +964,6 @@ 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); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1826,30 +1821,6 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } - } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else { - out_frame[0] = RESP_CODE_OK; - uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; - memcpy(&out_frame[1], &value, 1); - _serial->writeFrame(out_frame, 2); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { - uint8_t value = cmd_frame[1]; - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else if (value <= 1) { - _prefs.radio_fem_rxgain = value; - if (board.setLoRaFemLnaEnabled(value != 0)) { - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ecb117bd2f..84d51413b1 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,7 +29,6 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) - uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) From 5300fa18c7b0f441d9a123b86aa49a7a5c9739cf Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 13 Jun 2026 17:40:11 +0800 Subject: [PATCH 26/74] Restore companion NodePrefs file ending --- 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 84d51413b1..48c381ceaf 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -34,4 +34,4 @@ struct NodePrefs { // persisted to file uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; -}; +}; \ No newline at end of file From 07648e3344780bcac297d197ec1579a91a308fbf Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 14 Jun 2026 17:50:41 +1000 Subject: [PATCH 27/74] * fix for anon contacts when full --- examples/companion_radio/MyMesh.cpp | 12 ++------ examples/simple_secure_chat/main.cpp | 2 +- src/helpers/BaseChatMesh.cpp | 45 ++++++++++++++++------------ src/helpers/BaseChatMesh.h | 14 ++++++--- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c468967f97..d05bc6de58 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -854,7 +854,7 @@ void MyMesh::onSendTimeout() {} MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { _iter_started = false; _cli_rescue = false; offline_queue_len = 0; @@ -2187,15 +2187,7 @@ void MyMesh::checkSerialInterface() { && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! ) { ContactInfo contact; - bool found = false; - while (_iter.hasNext(this, contact)) { - if (contact.type != ADV_TYPE_NONE) { - found = true; - break; - } - } - - if (found) { + if (_iter.hasNext(this, contact)) { if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter writeContactRespFrame(RESP_CODE_CONTACT, contact); if (contact.lastmod > _most_recent_lastmod) { diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index d50667368c..d93810ed94 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -135,7 +135,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor { File file = _fs->open("/contacts", "w", true); #endif if (file) { - ContactsIterator iter; + ContactsIterator iter = startContactsIterator(); ContactInfo c; uint8_t unused = 0; uint32_t reserved = 0; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index d3ef034e67..71b2681f4a 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -68,29 +68,36 @@ void BaseChatMesh::bootstrapRTCfromContacts() { } ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) { - if (num_contacts < MAX_CONTACTS) { - return &contacts[num_contacts++]; - } else if (transient_only || shouldOverwriteWhenFull()) { - // Find oldest non-favourite contact by oldest lastmod timestamp - int oldest_idx = -1; - uint32_t oldest_lastmod = 0xFFFFFFFF; - for (int i = 0; i < num_contacts; i++) { - if (transient_only) { - if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) { - oldest_lastmod = contacts[i].lastmod; - oldest_idx = i; - } - } else { + int oldest_idx = -1; + uint32_t oldest_lastmod = 0xFFFFFFFF; + if (transient_only) { + // only allocate from first N + for (int i = 0; i < MAX_ANON_CONTACTS; i++) { + if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) { + oldest_lastmod = contacts[i].lastmod; + oldest_idx = i; + } + } + if (oldest_idx >= 0) { + // NOTE: do NOT call onContactOverwrite() + return &contacts[oldest_idx]; + } + } else { + if (num_contacts < MAX_ANON_CONTACTS+MAX_CONTACTS) { + return &contacts[num_contacts++]; + } else if (shouldOverwriteWhenFull()) { + // Find oldest non-favourite contact by oldest lastmod timestamp + for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) { bool is_favourite = (contacts[i].flags & 0x01) != 0; - if (!is_favourite && contacts[i].lastmod < oldest_lastmod && contacts[i].type != ADV_TYPE_NONE) { + if (!is_favourite && contacts[i].lastmod < oldest_lastmod) { oldest_lastmod = contacts[i].lastmod; oldest_idx = i; } } - } - if (oldest_idx >= 0) { - onContactOverwrite(contacts[oldest_idx].id.pub_key); - return &contacts[oldest_idx]; + if (oldest_idx >= 0) { + onContactOverwrite(contacts[oldest_idx].id.pub_key); + return &contacts[oldest_idx]; + } } } return NULL; // no space, no overwrite or all contacts are all favourites @@ -930,7 +937,7 @@ bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) { } ContactsIterator BaseChatMesh::startContactsIterator() { - return ContactsIterator(); + return ContactsIterator(MAX_ANON_CONTACTS); // start at offset, skip the anon entries } bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index c04bfda38c..3a277c1ea3 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -28,8 +28,9 @@ class ContactVisitor { class BaseChatMesh; class ContactsIterator { - int next_idx = 0; + int next_idx; public: + ContactsIterator(int start) { next_idx = start; } bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest); }; @@ -79,8 +80,9 @@ class BaseChatMesh : public mesh::Mesh { protected: BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, mgr, tables) - { - num_contacts = 0; + { + resetContacts(); + #ifdef MAX_GROUP_CHANNELS memset(channels, 0, sizeof(channels)); num_channels = 0; @@ -91,7 +93,11 @@ class BaseChatMesh : public mesh::Mesh { } void bootstrapRTCfromContacts(); - void resetContacts() { num_contacts = 0; } + + void resetContacts() { + memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0) + num_contacts = MAX_ANON_CONTACTS; // seed the first contacts for anon requests + } void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp); ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact From c2d223ff5512f2862b004d33b8b39e9b8340cffe Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 14 Jun 2026 18:12:08 +1000 Subject: [PATCH 28/74] * now handle the case where onAdvertRecv() should _replace_ the anon contact slot --- src/helpers/BaseChatMesh.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 71b2681f4a..ef5863030f 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -146,6 +146,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, packet->header = save; } + if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ? + memset(from, 0, sizeof(*from)); // clear the anon/temp slot + from = NULL; // do normal 'add' flow + } + bool is_new = false; // true = not in contacts[], false = exists in contacts[] if (from == NULL) { if (!shouldAutoAddContactType(parser.getType())) { From 538ac38e18c87181b884b93d7045752999253c21 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 14 Jun 2026 18:39:34 +1000 Subject: [PATCH 29/74] * fix for counter in RESP_CODE_CONTACTS_START --- src/helpers/BaseChatMesh.cpp | 2 +- src/helpers/BaseChatMesh.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index ef5863030f..972a97e9e6 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -946,7 +946,7 @@ ContactsIterator BaseChatMesh::startContactsIterator() { } bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) { - if (next_idx >= mesh->getNumContacts()) return false; + if (next_idx >= mesh->getTotalContactSlots()) return false; dest = mesh->contacts[next_idx++]; return true; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 3a277c1ea3..d987854709 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -172,7 +172,8 @@ class BaseChatMesh : public mesh::Mesh { ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len); bool removeContact(ContactInfo& contact); bool addContact(const ContactInfo& contact); - int getNumContacts() const { return num_contacts; } + int getTotalContactSlots() const { return num_contacts; } + int getNumContacts() const { return num_contacts - MAX_ANON_CONTACTS; } // don't include the reserved slots at start bool getContactByIdx(uint32_t idx, ContactInfo& contact); ContactsIterator startContactsIterator(); ChannelDetails* addChannel(const char* name, const char* psk_base64); From 2e73fe948eca9e289f8dd8900a251b027e33a014 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 14 Jun 2026 18:54:51 +1000 Subject: [PATCH 30/74] * fix for anon lastmod --- examples/companion_radio/MyMesh.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d05bc6de58..0b90df8bd9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1542,6 +1542,7 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE); anon.out_path_len = 0; // default to zero-hop direct anon.type = ADV_TYPE_NONE; // unknown + anon.lastmod = getRTCClock()->getCurrentTime(); if (addContact(anon)) recipient = &anon; } From e7db7c5a94680a1e983cf1c7aa2b558f85b72ce0 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 14 Jun 2026 11:41:01 +0200 Subject: [PATCH 31/74] Add ESP32 Reset Reason --- src/helpers/CommonCLI.cpp | 4 ---- src/helpers/ESP32Board.h | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5a5d1eabd5..25f1a2d19c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -944,13 +944,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "ERROR: Power management not supported"); #endif } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { -#ifdef NRF52_POWER_MANAGEMENT sprintf(reply, "> Reset: %s; Shutdown: %s", _board->getResetReasonString(_board->getResetReason()), _board->getShutdownReasonString(_board->getShutdownReason())); -#else - strcpy(reply, "ERROR: Power management not supported"); -#endif } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { #ifdef NRF52_POWER_MANAGEMENT sprintf(reply, "> %u mV", _board->getBootVoltage()); diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index a4cbf2a980..1efc99f364 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -155,6 +155,42 @@ class ESP32Board : public mesh::MainBoard { void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } + + uint32_t getResetReason() const override { + return esp_reset_reason(); + } + + // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html + const char* getResetReasonString(uint32_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: + return "Unknown or first boot"; + case ESP_RST_POWERON: + return "Power-on reset"; + case ESP_RST_EXT: + return "External reset"; + case ESP_RST_SW: + return "Software reset"; + case ESP_RST_PANIC: + return "Panic / exception reset"; + case ESP_RST_INT_WDT: + return "Interrupt watchdog reset"; + case ESP_RST_TASK_WDT: + return "Task watchdog reset"; + case ESP_RST_WDT: + return "Other watchdog reset"; + case ESP_RST_DEEPSLEEP: + return "Wake from deep sleep"; + case ESP_RST_BROWNOUT: + return "Brownout (low voltage)"; + case ESP_RST_SDIO: + return "SDIO reset"; + default: + static char buf[40]; + snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); + return buf; + } + } }; class ESP32RTCClock : public mesh::RTCClock { From 4f9a0916714baa2a34ef294289bff78809056676 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 18 Feb 2026 01:16:52 +0100 Subject: [PATCH 32/74] Use hardware channel activity detection for checking interference --- examples/companion_radio/MyMesh.cpp | 2 +- examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 2 +- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/radiolib/RadioLibWrappers.cpp | 13 ++++++++++--- src/helpers/radiolib/RadioLibWrappers.h | 3 ++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0b90df8bd9..2433f595e4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -259,7 +259,7 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return 1; // non-zero enables hardware CAD (Channel Activity Detection) before TX } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index af4706c13c..dd282ec814 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -892,7 +892,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX // bridge defaults _prefs.bridge_enabled = 1; // enabled diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 349efb4450..97ec80ac55 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -649,7 +649,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX #ifdef ROOM_PASSWORD StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index c0235a143f..2a2c7febc6 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -725,7 +725,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.flood_advert_interval = 0; // disabled _prefs.disable_fwd = true; _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX // GPS defaults _prefs.gps_enabled = 0; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b6519aefa7..c67ab768c5 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -178,10 +178,17 @@ void RadioLibWrapper::onSendFinished() { state = STATE_IDLE; } +int16_t RadioLibWrapper::performChannelScan() { + return _radio->scanChannel(); +} + bool RadioLibWrapper::isChannelActive() { - return _threshold == 0 - ? false // interference check is disabled - : getCurrentRSSI() > _noise_floor + _threshold; + if (_threshold == 0) return false; // interference check is disabled + + int16_t result = performChannelScan(); + // scanChannel() leaves radio in standby — restart RX regardless of result + startRecv(); + return (result == RADIOLIB_LORA_DETECTED); } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index efd3e17931..c1ece6448b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -32,7 +32,7 @@ class RadioLibWrapper : public mesh::Radio { bool isInRecvMode() const override; bool isChannelActive(); - bool isReceiving() override { + bool isReceiving() override { if (isReceivingPacket()) return true; return isChannelActive(); @@ -46,6 +46,7 @@ class RadioLibWrapper : public mesh::Radio { virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; From 813d108c7ab26e0b8e55087081ff84b9d62e91ba Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sun, 22 Feb 2026 16:05:18 +0100 Subject: [PATCH 33/74] Also return busy if preamble detected --- src/helpers/radiolib/RadioLibWrappers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index c67ab768c5..9b00b176bb 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -188,7 +188,7 @@ bool RadioLibWrapper::isChannelActive() { int16_t result = performChannelScan(); // scanChannel() leaves radio in standby — restart RX regardless of result startRecv(); - return (result == RADIOLIB_LORA_DETECTED); + return (result == RADIOLIB_LORA_DETECTED || result == RADIOLIB_PREAMBLE_DETECTED); } float RadioLibWrapper::getLastRSSI() const { From 04a6c7009ab26c3af2c2e1550c07201139235dc2 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sun, 22 Feb 2026 16:08:04 +0100 Subject: [PATCH 34/74] Just check for not channel free --- src/helpers/radiolib/RadioLibWrappers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 9b00b176bb..d50c2bace7 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -188,7 +188,7 @@ bool RadioLibWrapper::isChannelActive() { int16_t result = performChannelScan(); // scanChannel() leaves radio in standby — restart RX regardless of result startRecv(); - return (result == RADIOLIB_LORA_DETECTED || result == RADIOLIB_PREAMBLE_DETECTED); + return result != RADIOLIB_CHANNEL_FREE; } float RadioLibWrapper::getLastRSSI() const { From 099ef6734843261d5a542570d8d1c3f35dced36c Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 6 Mar 2026 04:09:05 +0100 Subject: [PATCH 35/74] Prevent packet errors from growing --- src/helpers/radiolib/RadioLibWrappers.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index d50c2bace7..5d68d7e90e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -186,7 +186,10 @@ bool RadioLibWrapper::isChannelActive() { if (_threshold == 0) return false; // interference check is disabled int16_t result = performChannelScan(); - // scanChannel() leaves radio in standby — restart RX regardless of result + // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY + // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't + // try to read a non-existent packet and count a spurious recv error. + state = STATE_IDLE; startRecv(); return result != RADIOLIB_CHANNEL_FREE; } From 7c8e09245760c71a64f14ca46b4ff6b8dcdd3aa9 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 3 Jun 2026 10:13:10 +0200 Subject: [PATCH 36/74] Have CAD be a separate toggle (set cad on/off) --- docs/cli_commands.md | 14 ++++++++++++++ examples/companion_radio/MyMesh.cpp | 5 ++++- examples/companion_radio/MyMesh.h | 1 + examples/simple_repeater/MyMesh.cpp | 3 ++- examples/simple_repeater/MyMesh.h | 3 +++ examples/simple_room_server/MyMesh.cpp | 3 ++- examples/simple_room_server/MyMesh.h | 3 +++ examples/simple_sensor/SensorMesh.cpp | 6 +++++- examples/simple_sensor/SensorMesh.h | 1 + src/Dispatcher.cpp | 1 + src/Dispatcher.h | 3 +++ src/helpers/CommonCLI.cpp | 13 +++++++++++-- src/helpers/CommonCLI.h | 1 + src/helpers/radiolib/RadioLibWrappers.cpp | 23 +++++++++++++++-------- src/helpers/radiolib/RadioLibWrappers.h | 2 ++ 15 files changed, 68 insertions(+), 14 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ce0e7da18b..c06f5e12b3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -578,6 +578,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Enable or disable hardware Channel Activity Detection (CAD) +**Usage:** +- `get cad` +- `set cad ` + +**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` — either, both, or none may be active. + +**Parameters:** +- `on|off`: Enable or disable hardware CAD + +**Default:** `off` + +--- + #### View or change the AGC Reset Interval **Usage:** - `get agc.reset.interval` diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2433f595e4..5fb9bf9d37 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -259,7 +259,10 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 1; // non-zero enables hardware CAD (Channel Activity Detection) before TX + return 0; // disabled for now, until currentRSSI() problem is resolved +} +bool MyMesh::getCADEnabled() const { + return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default) } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 43d3950beb..f4190f30ac 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -105,6 +105,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; + bool getCADEnabled() const override; 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/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index dd282ec814..5cc3a9a11e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -892,7 +892,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; - _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX + _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // bridge defaults _prefs.bridge_enabled = 1; // enabled diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 7597c6c6f6..24c4b1f2a7 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -150,6 +150,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 97ec80ac55..12d0b0c318 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -649,7 +649,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; - _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX + _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') #ifdef ROOM_PASSWORD StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5277ddad61..e9e53ec919 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -144,6 +144,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 2a2c7febc6..59c9aa0900 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -323,6 +323,9 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { int SensorMesh::getInterferenceThreshold() const { return _prefs.interference_threshold; } +bool SensorMesh::getCADEnabled() const { + return _prefs.cad_enabled; +} int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } @@ -725,7 +728,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.flood_advert_interval = 0; // disabled _prefs.disable_fwd = true; _prefs.flood_max = 64; - _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX + _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // GPS defaults _prefs.gps_enabled = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index c9f135f65e..1d65b8772b 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -120,6 +120,7 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; + bool getCADEnabled() const override; int getAGCResetInterval() const 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; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..c0610b7f8a 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -66,6 +66,7 @@ uint32_t Dispatcher::getCADFailMaxDuration() const { void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); + _radio->setCADEnabled(getCADEnabled()); next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); } _radio->loop(); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index dd032f130d..aad6cba3ec 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -65,6 +65,8 @@ class Radio { virtual void triggerNoiseFloorCalibrate(int threshold) { } + virtual void setCADEnabled(bool enable) { } + virtual void resetAGC() { } virtual bool isInRecvMode() const = 0; @@ -166,6 +168,7 @@ class Dispatcher { virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; virtual int getInterferenceThreshold() const { return 0; } // disabled by default + virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5a5d1eabd5..82e5374352 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -92,7 +92,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 - // next: 294 + file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 + // next: 295 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -123,6 +124,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean + _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean file.close(); } @@ -187,7 +189,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 - // next: 294 + file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 + // next: 295 file.close(); } @@ -503,6 +506,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _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(); @@ -801,6 +808,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep 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) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffeadc5bd1..10cb00c776 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -64,6 +64,7 @@ struct NodePrefs { // persisted to file uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) }; class CommonCLICallbacks { diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5d68d7e90e..5e72336c05 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -36,6 +36,7 @@ void RadioLibWrapper::begin() { _noise_floor = 0; _threshold = 0; + _cad_enabled = false; // start average out some samples _num_floor_samples = 0; @@ -183,15 +184,21 @@ int16_t RadioLibWrapper::performChannelScan() { } bool RadioLibWrapper::isChannelActive() { - if (_threshold == 0) return false; // interference check is disabled + // int.thresh: RSSI-based interference detection (relative to noise floor) + if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + + // cad: hardware channel activity detection + if (_cad_enabled) { + int16_t result = performChannelScan(); + // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY + // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't + // try to read a non-existent packet and count a spurious recv error. + state = STATE_IDLE; + startRecv(); + if (result != RADIOLIB_CHANNEL_FREE) return true; + } - int16_t result = performChannelScan(); - // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY - // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't - // try to read a non-existent packet and count a spurious recv error. - state = STATE_IDLE; - startRecv(); - return result != RADIOLIB_CHANNEL_FREE; + return false; } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index c1ece6448b..9943bcab77 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -9,6 +9,7 @@ class RadioLibWrapper : public mesh::Radio { mesh::MainBoard* _board; uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; + bool _cad_enabled; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; @@ -50,6 +51,7 @@ class RadioLibWrapper : public mesh::Radio { int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; + void setCADEnabled(bool enable) override { _cad_enabled = enable; } void resetAGC() override; void loop() override; From dd9b3b32c39876ad6b45090e48ef185e3c83d461 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 15 Jun 2026 00:57:12 +1000 Subject: [PATCH 37/74] build fix for Heltec Mesh Solar and Mesh Pocket --- variants/heltec_mesh_solar/platformio.ini | 1 - variants/mesh_pocket/platformio.ini | 3 --- 2 files changed, 4 deletions(-) diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index 92d641a357..bd0423228e 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -1,7 +1,6 @@ [Heltec_mesh_solar] extends = nrf52_base board = heltec_mesh_solar -platform_packages = framework-arduinoadafruitnrf52 board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index 52a0d83523..0d2a74adf5 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -1,7 +1,6 @@ [Mesh_pocket] extends = nrf52_base board = heltec_mesh_pocket -platform_packages = framework-arduinoadafruitnrf52 board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 @@ -32,7 +31,6 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 zinggjm/GxEPD2 @ 1.6.2 bakercp/CRC32 @ ^2.0.0 - debug_tool = jlink upload_protocol = nrfutil @@ -40,7 +38,6 @@ upload_protocol = nrfutil extends = Mesh_pocket build_src_filter = ${Mesh_pocket.build_src_filter} +<../examples/simple_repeater> - build_flags = ${Mesh_pocket.build_flags} -D ADVERT_NAME='"Heltec_Mesh_Pocket Repeater"' From 62849ef1142369f76c0975d709a23288b5a9b861 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 22 Jun 2026 09:13:36 +0700 Subject: [PATCH 38/74] Fixed GPS time sync for Heltec T114 --- variants/heltec_t114/target.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t114/target.cpp b/variants/heltec_t114/target.cpp index cb8f75db51..90a902f804 100644 --- a/variants/heltec_t114/target.cpp +++ b/variants/heltec_t114/target.cpp @@ -22,7 +22,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include -MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; From b975180fc88e441041ec52ffa49b36b0a215e1b0 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 25 Jun 2026 16:03:27 +1200 Subject: [PATCH 39/74] initial support for elecrow thinknode m7 --- boards/thinknode_m7.json | 42 +++++++ variants/thinknode_m7/ThinkNodeM7Board.cpp | 17 +++ variants/thinknode_m7/ThinkNodeM7Board.h | 23 ++++ variants/thinknode_m7/pins_arduino.h | 19 +++ variants/thinknode_m7/platformio.ini | 134 +++++++++++++++++++++ variants/thinknode_m7/target.cpp | 84 +++++++++++++ variants/thinknode_m7/target.h | 29 +++++ variants/thinknode_m7/variant.cpp | 8 ++ variants/thinknode_m7/variant.h | 1 + 9 files changed, 357 insertions(+) create mode 100644 boards/thinknode_m7.json create mode 100644 variants/thinknode_m7/ThinkNodeM7Board.cpp create mode 100644 variants/thinknode_m7/ThinkNodeM7Board.h create mode 100644 variants/thinknode_m7/pins_arduino.h create mode 100644 variants/thinknode_m7/platformio.ini create mode 100644 variants/thinknode_m7/target.cpp create mode 100644 variants/thinknode_m7/target.h create mode 100644 variants/thinknode_m7/variant.cpp create mode 100644 variants/thinknode_m7/variant.h diff --git a/boards/thinknode_m7.json b/boards/thinknode_m7.json new file mode 100644 index 0000000000..2a0c5e5838 --- /dev/null +++ b/boards/thinknode_m7.json @@ -0,0 +1,42 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-D BOARD_HAS_PSRAM", + "-D ARDUINO_USB_CDC_ON_BOOT=0", + "-D ARDUINO_USB_MODE=0", + "-D ARDUINO_RUNNING_CORE=1", + "-D ARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "qio_opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "ELECROW-ThinkNode-M7" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "ELECROW ThinkNode M7", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 524288, + "maximum_size": 8388608, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.elecrow.com", + "vendor": "ELECROW" +} diff --git a/variants/thinknode_m7/ThinkNodeM7Board.cpp b/variants/thinknode_m7/ThinkNodeM7Board.cpp new file mode 100644 index 0000000000..6b1bf86029 --- /dev/null +++ b/variants/thinknode_m7/ThinkNodeM7Board.cpp @@ -0,0 +1,17 @@ +#include "ThinkNodeM7Board.h" + +void ThinkNodeM7Board::begin() { + ESP32Board::begin(); +} + +void ThinkNodeM7Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_deep_sleep_start(); +} + +void ThinkNodeM7Board::powerOff() { + enterDeepSleep(0); +} + +const char* ThinkNodeM7Board::getManufacturerName() const { + return "Elecrow ThinkNode M7"; +} diff --git a/variants/thinknode_m7/ThinkNodeM7Board.h b/variants/thinknode_m7/ThinkNodeM7Board.h new file mode 100644 index 0000000000..f591b5accc --- /dev/null +++ b/variants/thinknode_m7/ThinkNodeM7Board.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include "variant.h" +#include "NullDisplayDriver.h" +#include "MomentaryButton.h" + +class ThinkNodeM7Board : public ESP32Board { + +public: + void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; + const char* getManufacturerName() const override; + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); + } +}; diff --git a/variants/thinknode_m7/pins_arduino.h b/variants/thinknode_m7/pins_arduino.h new file mode 100644 index 0000000000..845a8db1d5 --- /dev/null +++ b/variants/thinknode_m7/pins_arduino.h @@ -0,0 +1,19 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 17; +static const uint8_t SCL = 18; + +// Default SPI is the LR1110 radio bus +static const uint8_t SS = 12; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 9; +static const uint8_t SCK = 11; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini new file mode 100644 index 0000000000..ea421b07df --- /dev/null +++ b/variants/thinknode_m7/platformio.ini @@ -0,0 +1,134 @@ +[ThinkNode_M7] +extends = esp32_base +board = thinknode_m7 +build_flags = ${esp32_base.build_flags} + -I src/helpers/esp32 + -I variants/thinknode_m7 + -I src/helpers/sensors + -I src/helpers/ui + -D THINKNODE_M7 + -D PIN_USER_BTN_ANA=4 + -D PIN_STATUS_LED=3 ; green + -D LED_STATE_ON=LOW + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D USE_LR1110 + -D LORA_TX_POWER=22 + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=11 + -D P_LORA_NSS=12 + -D P_LORA_DIO_1=38 + -D P_LORA_MISO=9 + -D P_LORA_MOSI=10 + -D P_LORA_RESET=39 + -D P_LORA_TX_LED=46 ; blue + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 +build_src_filter = ${esp32_base.build_src_filter} + + + + + + + +<../variants/thinknode_m7> +lib_deps = ${esp32_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M7_repeater] +extends = ThinkNode_M7 +build_src_filter = ${ThinkNode_M7.build_src_filter} + +<../examples/simple_repeater/*.cpp> +build_flags = + ${ThinkNode_M7.build_flags} + -D ADVERT_NAME='"ThinkNode M7 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${ThinkNode_M7.lib_deps} + ${esp32_ota.lib_deps} + +[env:ThinkNode_M7_room_server] +extends = ThinkNode_M7 +build_src_filter = ${ThinkNode_M7.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${ThinkNode_M7.build_flags} + -D ADVERT_NAME='"ThinkNode M7 Room Server"' + -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 +lib_deps = + ${ThinkNode_M7.lib_deps} + ${esp32_ota.lib_deps} + +[env:ThinkNode_M7_companion_radio_ble] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + -I examples/companion_radio/ui-orig + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 +build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = + ${ThinkNode_M7.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:ThinkNode_M7_companion_radio_usb] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = + ${ThinkNode_M7.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:ThinkNode_M7_companion_radio_wifi] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=NullDisplayDriver + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 + -D MESH_PACKET_LOGGING=1 +build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = + ${ThinkNode_M7.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:ThinkNode_M7_kiss_modem] +extends = ThinkNode_M7 +build_src_filter = ${ThinkNode_M7.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m7/target.cpp b/variants/thinknode_m7/target.cpp new file mode 100644 index 0000000000..f4b898d8e9 --- /dev/null +++ b/variants/thinknode_m7/target.cpp @@ -0,0 +1,84 @@ +#include +#include "target.h" +#include + +ThinkNodeM7Board board; + +static SPIClass spi; +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); + +ESP32RTCClock 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; +#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, + END_OF_MODE_TABLE, +}; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#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.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS); + + 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 +} + + diff --git a/variants/thinknode_m7/target.h b/variants/thinknode_m7/target.h new file mode 100644 index 0000000000..34213f57ca --- /dev/null +++ b/variants/thinknode_m7/target.h @@ -0,0 +1,29 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +extern ThinkNodeM7Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + + + \ No newline at end of file diff --git a/variants/thinknode_m7/variant.cpp b/variants/thinknode_m7/variant.cpp new file mode 100644 index 0000000000..e056ab647a --- /dev/null +++ b/variants/thinknode_m7/variant.cpp @@ -0,0 +1,8 @@ +#include "variant.h" +#include "Arduino.h" + +void initVariant() +{ + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, HIGH); +} \ No newline at end of file diff --git a/variants/thinknode_m7/variant.h b/variants/thinknode_m7/variant.h new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/variants/thinknode_m7/variant.h @@ -0,0 +1 @@ + From 5d0ff7fe6e402704dd8c3ad10199ca8054c731b2 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 26 Jun 2026 07:06:37 +1000 Subject: [PATCH 40/74] remove guard on MyMesh::setRxBoostedGain() --- examples/simple_repeater/MyMesh.cpp | 2 -- examples/simple_repeater/MyMesh.h | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5cc3a9a11e..5f92845280 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1062,11 +1062,9 @@ void MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); } -#if defined(USE_SX1262) || defined(USE_SX1268) void MyMesh::setRxBoostedGain(bool enable) { radio_driver.setRxBoostedGainMode(enable); } -#endif void MyMesh::formatNeighborsReply(char *reply) { char *dp = reply; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 24c4b1f2a7..e66f7e596f 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -252,7 +252,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { // To check if there is pending work bool hasPendingWork() const; -#if defined(USE_SX1262) || defined(USE_SX1268) void setRxBoostedGain(bool enable) override; -#endif + }; From 9c7d71b9be9115e776d7aae28030d09023813a84 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Mon, 8 Dec 2025 11:21:29 +0100 Subject: [PATCH 41/74] Added support for the nibble zero connect --- variants/nibble_zero_connect/platformio.ini | 163 ++++++++++++++++++++ variants/nibble_zero_connect/target.cpp | 50 ++++++ variants/nibble_zero_connect/target.h | 31 ++++ 3 files changed, 244 insertions(+) create mode 100644 variants/nibble_zero_connect/platformio.ini create mode 100644 variants/nibble_zero_connect/target.cpp create mode 100644 variants/nibble_zero_connect/target.h diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini new file mode 100644 index 0000000000..4e40ff096c --- /dev/null +++ b/variants/nibble_zero_connect/platformio.ini @@ -0,0 +1,163 @@ +[nibble_zero_connect_base] +extends = esp32_base +board = esp32-s3-zero +build_flags = + ${esp32_base.build_flags} + -I variants/nibble_zero_connect + -D NIBBLE_ZERO_CONNECT + -D P_LORA_DIO_1=4 + -D P_LORA_NSS=10 + -D P_LORA_RESET=6 + -D P_LORA_BUSY=5 + -D P_LORA_SCLK=12 + -D P_LORA_MISO=13 + -D P_LORA_MOSI=11 + -D PIN_USER_BTN=1 + -D PIN_BOARD_SDA=8 + -D PIN_BOARD_SCL=7 + -D PIN_STATUS_LED=39 + -D P_LORA_TX_LED=39 + -D DISPLAY_ROTATION=0 + -D HAS_NEOPIXEL + -D NEOPIXEL_COUNT=1 + -D NEOPIXEL_DATA=17 + -D NEOPIXEL_TYPE=(NEO_GRB+NEO_KHZ800) + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/nibble_zero_connect> +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + adafruit/Adafruit NeoPixel @ ^1.12.3 + +[env:nibble_zero_connect_repeater] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Nibble Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + ${esp32_ota.lib_deps} + +[env:nibble_zero_connect_repeater_bridge_espnow] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/simple_repeater> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + ${esp32_ota.lib_deps} + +[env:nibble_zero_connect_terminal_chat] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:nibble_zero_connect_room_server] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Nibble Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + ${esp32_ota.lib_deps} + +[env:nibble_zero_connect_companion_radio_usb] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=8 +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:nibble_zero_connect_companion_radio_ble] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:nibble_zero_connect_companion_radio_wifi] +extends = nibble_zero_connect_base +build_flags = + ${nibble_zero_connect_base.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=8 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${nibble_zero_connect_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + + diff --git a/variants/nibble_zero_connect/target.cpp b/variants/nibble_zero_connect/target.cpp new file mode 100644 index 0000000000..73ac0a8483 --- /dev/null +++ b/variants/nibble_zero_connect/target.cpp @@ -0,0 +1,50 @@ +#include +#include "target.h" + +ESP32Board board; + +static SPIClass spi; +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); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + return radio.std_init(&spi); +} + +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(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} + + diff --git a/variants/nibble_zero_connect/target.h b/variants/nibble_zero_connect/target.h new file mode 100644 index 0000000000..ea558f63cb --- /dev/null +++ b/variants/nibble_zero_connect/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#endif + +extern ESP32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +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(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); + + From 69df5c7a9588b6fcb436f474d63179074ea08abb Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Thu, 5 Feb 2026 08:51:16 +0100 Subject: [PATCH 42/74] Remove NeoPixel configuration as its unused. --- variants/nibble_zero_connect/platformio.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 4e40ff096c..148019208c 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -18,10 +18,6 @@ build_flags = -D PIN_STATUS_LED=39 -D P_LORA_TX_LED=39 -D DISPLAY_ROTATION=0 - -D HAS_NEOPIXEL - -D NEOPIXEL_COUNT=1 - -D NEOPIXEL_DATA=17 - -D NEOPIXEL_TYPE=(NEO_GRB+NEO_KHZ800) -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 @@ -34,7 +30,6 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 - adafruit/Adafruit NeoPixel @ ^1.12.3 [env:nibble_zero_connect_repeater] extends = nibble_zero_connect_base From eb938a9b78f1d8b4b46d694a955115fc001bcd6e Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Tue, 12 May 2026 09:01:38 +0200 Subject: [PATCH 43/74] Address review comments and sync with dev --- variants/nibble_zero_connect/platformio.ini | 14 +++++++------- variants/nibble_zero_connect/target.cpp | 17 +---------------- variants/nibble_zero_connect/target.h | 3 --- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 148019208c..83495cd839 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -31,7 +31,7 @@ lib_deps = ${esp32_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 -[env:nibble_zero_connect_repeater] +[env:nibble_zero_connect_repeater_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -48,7 +48,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_zero_connect_repeater_bridge_espnow] +[env:nibble_zero_connect_repeater_bridge_espnow_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -67,7 +67,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_zero_connect_terminal_chat] +[env:nibble_zero_connect_terminal_chat_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -79,7 +79,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_zero_connect_room_server] +[env:nibble_zero_connect_room_server_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -96,7 +96,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_zero_connect_companion_radio_usb] +[env:nibble_zero_connect_companion_radio_usb_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -113,7 +113,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_zero_connect_companion_radio_ble] +[env:nibble_zero_connect_companion_radio_ble_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} @@ -134,7 +134,7 @@ lib_deps = ${nibble_zero_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_zero_connect_companion_radio_wifi] +[env:nibble_zero_connect_companion_radio_wifi_] extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} diff --git a/variants/nibble_zero_connect/target.cpp b/variants/nibble_zero_connect/target.cpp index 73ac0a8483..d53b6a1c3d 100644 --- a/variants/nibble_zero_connect/target.cpp +++ b/variants/nibble_zero_connect/target.cpp @@ -13,7 +13,7 @@ SensorManager sensors; #ifdef DISPLAY_CLASS DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif #ifndef LORA_CR @@ -27,21 +27,6 @@ bool radio_init() { return radio.std_init(&spi); } -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(uint8_t dbm) { - radio.setOutputPower(dbm); -} - mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); diff --git a/variants/nibble_zero_connect/target.h b/variants/nibble_zero_connect/target.h index ea558f63cb..87c55f0f2e 100644 --- a/variants/nibble_zero_connect/target.h +++ b/variants/nibble_zero_connect/target.h @@ -23,9 +23,6 @@ extern SensorManager sensors; #endif 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(uint8_t dbm); mesh::LocalIdentity radio_new_identity(); From b8f1fad65446bb22803b6a74b7bbbd6a99a6f761 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Thu, 25 Jun 2026 23:26:53 +0200 Subject: [PATCH 44/74] Add trailing underscore to nibble_screen_connect env names to exclude from automatic builds --- variants/nibble_screen_connect/platformio.ini | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 20f7dc2438..6a2f3dec2a 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -34,7 +34,7 @@ lib_deps = adafruit/Adafruit SSD1306 @ ^2.5.13 adafruit/Adafruit NeoPixel @ ^1.12.3 -[env:nibble_screen_connect_repeater] +[env:nibble_screen_connect_repeater_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -51,7 +51,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_screen_connect_repeater_bridge_espnow] +[env:nibble_screen_connect_repeater_bridge_espnow_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -70,7 +70,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_screen_connect_terminal_chat] +[env:nibble_screen_connect_terminal_chat_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -82,7 +82,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_screen_connect_room_server] +[env:nibble_screen_connect_room_server_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -99,7 +99,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} ${esp32_ota.lib_deps} -[env:nibble_screen_connect_companion_radio_usb] +[env:nibble_screen_connect_companion_radio_usb_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -116,7 +116,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_screen_connect_companion_radio_ble] +[env:nibble_screen_connect_companion_radio_ble_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -137,7 +137,7 @@ lib_deps = ${nibble_screen_connect_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:nibble_screen_connect_companion_radio_wifi] +[env:nibble_screen_connect_companion_radio_wifi_] extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} @@ -160,7 +160,7 @@ lib_deps = densaugeo/base64 @ ~1.4.0 -[env:nibble_screen_connect_kiss_modem] +[env:nibble_screen_connect_kiss_modem_] extends = nibble_screen_connect_base build_src_filter = ${nibble_screen_connect_base.build_src_filter} +<../examples/kiss_modem/> From 1f9bb6740048c08708718e3b26b628af98fe3ff1 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 26 Jun 2026 07:44:01 +1000 Subject: [PATCH 45/74] return bool when setting rx boost --- examples/simple_repeater/MyMesh.cpp | 4 ++-- examples/simple_repeater/MyMesh.h | 2 +- src/helpers/CommonCLI.h | 4 ++-- src/helpers/esp32/ESPNOWRadio.h | 2 +- src/helpers/radiolib/CustomLLCC68Wrapper.h | 4 ++-- src/helpers/radiolib/CustomLR1110Wrapper.h | 4 ++-- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 ++-- src/helpers/radiolib/CustomSX1268Wrapper.h | 4 ++-- src/helpers/radiolib/RadioLibWrappers.h | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5f92845280..ca4cfad27d 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1062,8 +1062,8 @@ void MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); } -void MyMesh::setRxBoostedGain(bool enable) { - radio_driver.setRxBoostedGainMode(enable); +bool MyMesh::setRxBoostedGain(bool enable) { + return radio_driver.setRxBoostedGainMode(enable); } void MyMesh::formatNeighborsReply(char *reply) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index e66f7e596f..fb091a4cf6 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -252,6 +252,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { // To check if there is pending work bool hasPendingWork() const; - void setRxBoostedGain(bool enable) override; + bool setRxBoostedGain(bool enable) override; }; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 10cb00c776..f3abcf4772 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -111,8 +111,8 @@ class CommonCLICallbacks { // no op by default }; - virtual void setRxBoostedGain(bool enable) { - // no op by default + virtual bool setRxBoostedGain(bool enable) { + return false; // CommonCLI reports unsupported if not overridden by wrapper }; }; diff --git a/src/helpers/esp32/ESPNOWRadio.h b/src/helpers/esp32/ESPNOWRadio.h index 67b1448e89..f474215a15 100644 --- a/src/helpers/esp32/ESPNOWRadio.h +++ b/src/helpers/esp32/ESPNOWRadio.h @@ -38,7 +38,7 @@ class ESPNOWRadio : public mesh::Radio { * These two functions do nothing for ESP-NOW, but are needed for the * Radio interface. */ - virtual void setRxBoostedGainMode(bool) { } + virtual bool setRxBoostedGainMode(bool) { } virtual bool getRxBoostedGainMode() const { return false; } uint32_t intID(); diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 8861f76d24..851fd644b1 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -33,8 +33,8 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } - void setRxBoostedGainMode(bool en) override { - ((CustomLLCC68 *)_radio)->setRxBoostedGainMode(en); + bool setRxBoostedGainMode(bool en) override { + return ((CustomLLCC68 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomLLCC68 *)_radio)->getRxBoostedGainMode(); diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 13efd25b57..fc50528395 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -36,8 +36,8 @@ class CustomLR1110Wrapper : public RadioLibWrapper { uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } - void setRxBoostedGainMode(bool en) override { - ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en); + bool setRxBoostedGainMode(bool en) override { + return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomLR1110 *)_radio)->getRxBoostedGainMode(); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb2238b..1d103f5790 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -40,8 +40,8 @@ class CustomSX1262Wrapper : public RadioLibWrapper { void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } - void setRxBoostedGainMode(bool en) override { - ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en); + bool setRxBoostedGainMode(bool en) override { + return ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomSX1262 *)_radio)->getRxBoostedGainMode(); diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 9ddea78f3f..bce56b9963 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -37,8 +37,8 @@ class CustomSX1268Wrapper : public RadioLibWrapper { void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } - void setRxBoostedGainMode(bool en) override { - ((CustomSX1268 *)_radio)->setRxBoostedGainMode(en); + bool setRxBoostedGainMode(bool en) override { + return ((CustomSX1268 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomSX1268 *)_radio)->getRxBoostedGainMode(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab77..3091832f11 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -66,7 +66,7 @@ class RadioLibWrapper : public mesh::Radio { float packetScore(float snr, int packet_len) override { return packetScoreInt(snr, 10, packet_len); } // assume sf=10 - virtual void setRxBoostedGainMode(bool) { } + virtual bool setRxBoostedGainMode(bool) { return false; } virtual bool getRxBoostedGainMode() const { return false; } }; From b07aba793724e24b52138ea2a7172d9a3bd34e91 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 26 Jun 2026 08:45:03 +1000 Subject: [PATCH 46/74] fix: report error when rxgain setting is unsupported or rejected --- src/helpers/CommonCLI.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index dce1c5d894..f82d40e25d 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -571,13 +571,15 @@ 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"); -#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; - strcpy(reply, "OK"); - savePrefs(); - _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); -#endif + bool enabled = memcmp(&config[13], "on", 2) == 0; + if (_callbacks->setRxBoostedGain(enabled)) { + _prefs->rx_boosted_gain = enabled; + strcpy(reply, "OK"); + savePrefs(); + } else { + strcpy(reply, "Error: unsupported or rejected"); + } } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); @@ -835,10 +837,8 @@ 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)); -#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); -#endif } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); From f3d4d8cd5eda7428435a1a7769e3891cdab67344 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 26 Jun 2026 22:36:49 +1000 Subject: [PATCH 47/74] always save boosted gain setting --- src/helpers/CommonCLI.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index f82d40e25d..c95e3e34b0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -573,13 +573,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep 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)) { - _prefs->rx_boosted_gain = enabled; strcpy(reply, "OK"); - savePrefs(); } else { - strcpy(reply, "Error: unsupported or rejected"); - } + strcpy(reply, "Error: unsupported"); + } } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); From 4f8cb8db78c908bc350cd990f116a3ff7d4b9976 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 27 Jun 2026 21:06:01 +1000 Subject: [PATCH 48/74] * ACK packets being 'clipped' (in Dispatcher send). Needed to extend max_airtime timeout calc for short packets --- src/helpers/radiolib/CustomLR1110Wrapper.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index fc50528395..c6b1acb472 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -26,6 +26,11 @@ class CustomLR1110Wrapper : public RadioLibWrapper { return rssi; } + uint32_t getEstAirtimeFor(int len_bytes) override { + auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes); + return airtime < 200 ? 200 : airtime; // at least 200 millis + } + void onSendFinished() override { RadioLibWrapper::onSendFinished(); _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts From bc35208682126f55f2ceae7c55ce8237d3ec51e2 Mon Sep 17 00:00:00 2001 From: HDDen <62592944+HDDen@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:32:22 +0300 Subject: [PATCH 49/74] Update number_allocations.md Added Data-Type range for MeshCore Images codec (range is for small reserve to future versions) --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 857525d468..f35f3b2d96 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,6 +17,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | +| 0120 - 0121 | MCOimg | most.original.address@gmail.com — https://hdden.ru/MCOimg/ | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 58b0f7df9cce97987540c84454131e090bb00dd7 Mon Sep 17 00:00:00 2001 From: HDDen <62592944+HDDen@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:41:44 +0300 Subject: [PATCH 50/74] Update number_allocations.md --- docs/number_allocations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index f35f3b2d96..2ccf950f4f 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,7 +17,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | -| 0120 - 0121 | MCOimg | most.original.address@gmail.com — https://hdden.ru/MCOimg/ | +| 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 2cb3a29fa86971fceab26ac72f229316b40b474b Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:55:16 +1000 Subject: [PATCH 51/74] SX1262 LDO Support - Added build flag "SX126X_USE_REGULATOR_LDO" (set to 1 to enable) to use LDO for the SX1262 radio instead of DC-DC when the DCDC pin isn't wired up (e.g. t-echo lite) - Added additional debug mode diagnostic messages during SX1262 init to better highlight faults --- src/helpers/radiolib/CustomSX1262.h | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ad20122902..ca62fc26af 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -1,6 +1,7 @@ #pragma once #include +#include "MeshCore.h" #define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received #define SX126X_IRQ_PREAMBLE_DETECTED 0x04 @@ -27,6 +28,14 @@ class CustomSX1262 : public SX1262 { uint8_t cr = 5; #endif + #ifdef SX126X_USE_REGULATOR_LDO + constexpr bool useRegulatorLDO = SX126X_USE_REGULATOR_LDO; + #else + constexpr bool useRegulatorLDO = false; + #endif + + MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC"); + #if defined(P_LORA_SCLK) #ifdef NRF52_PLATFORM if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); } @@ -42,11 +51,12 @@ class CustomSX1262 : public SX1262 { if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); #endif #endif - int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo, useRegulatorLDO); // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { + MESH_DEBUG_PRINTLN("SX1262 init failed with error %d, retrying with TCXO at 0.0V", status); tcxo = 0.0f; - status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo, useRegulatorLDO); } if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); @@ -83,6 +93,8 @@ class CustomSX1262 : public SX1262 { writeRegister(0x8B5, &r_data, 1); #endif + MESH_DEBUG_PRINTLN("SX1262 status=0x%02X device_errors=0x%04X", getStatus(), getDeviceErrors()); + return true; // success } From 4f7c1cb88f25578633ebd7bc5755f71d888c168b Mon Sep 17 00:00:00 2001 From: Christoph Koehler Date: Wed, 17 Jun 2026 08:55:54 -0600 Subject: [PATCH 52/74] build: add gcc, gtest to nix shell, fix native test framework --- default.nix | 7 ++++--- platformio.ini | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/default.nix b/default.nix index 4f9e1c59b8..13d410de3e 100644 --- a/default.nix +++ b/default.nix @@ -1,11 +1,12 @@ -{ pkgs ? import {} }: -let +{pkgs ? import {}}: let in pkgs.mkShell { buildInputs = [ pkgs.platformio pkgs.python3 + pkgs.gcc + pkgs.gtest # optional: needed as a programmer i.e. for esp32 pkgs.avrdude ]; -} + } diff --git a/platformio.ini b/platformio.ini index e16f7b8304..b08854e5d2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -157,6 +157,7 @@ lib_deps = [env:native] platform = native +test_framework = googletest build_flags = -std=c++17 -I src -I test/mocks From 4f701b7aec539eee31d98aadfea79063866e53e8 Mon Sep 17 00:00:00 2001 From: Christoph Koehler Date: Wed, 1 Jul 2026 20:01:58 -0600 Subject: [PATCH 53/74] refactor: split MeshTables::hasSeen into pure query + markSeen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasSeen() was simultaneously a predicate and a mutator — it inserted the packet hash on every miss, making five call sites that only wanted to mark a packet as sent call it with the return value discarded. Split into: - wasSeen() — pure predicate, no side effects - markSeen() — explicit insert All query sites now call markSeen() immediately after wasSeen() returns false, preserving identical runtime behaviour. The five mark-only send sites (sendFlood, sendDirect, sendZeroHop x2) now call markSeen directly. Also fixes three bridge sites (BridgeBase, ESPNowBridge, RS232Bridge) that had the same query+implicit-insert pattern. Tests: add test/test_mesh_tables/ covering wasSeen purity, markSeen, dup stats, and clear. Update SHA256 mock to produce deterministic output (previously finalize() was a no-op). Add Packet.cpp to native build filter. --- platformio.ini | 1 + src/Mesh.cpp | 43 +++++--- src/Mesh.h | 5 +- src/helpers/SimpleMeshTables.h | 16 ++- src/helpers/bridges/BridgeBase.cpp | 3 +- src/helpers/bridges/BridgeBase.h | 2 +- src/helpers/bridges/ESPNowBridge.cpp | 3 +- src/helpers/bridges/RS232Bridge.cpp | 3 +- test/mocks/SHA256.h | 29 ++++- .../test_simple_mesh_tables.cpp | 103 ++++++++++++++++++ 10 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 test/test_mesh_tables/test_simple_mesh_tables.cpp diff --git a/platformio.ini b/platformio.ini index b08854e5d2..47cc0ab8c3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -165,5 +165,6 @@ test_build_src = yes build_src_filter = -<*> +<../src/Utils.cpp> + +<../src/Packet.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/Mesh.cpp b/src/Mesh.cpp index e9b92262ce..c11f37cacf 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -55,7 +55,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint16_t offset = (uint16_t)pkt->path_len << path_sz; if (offset >= len) { // TRACE has reached end of given path onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); // append SNR (Not hash!) pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); @@ -89,14 +90,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - if (!_tables->hasSeen(pkt)) { // don't retransmit! + if (!_tables->wasSeen(pkt)) { // don't retransmit! + _tables->markSeen(pkt); removeSelfFromPath(pkt); routeDirectRecvAcks(pkt, 0); } return ACTION_RELEASE; } - if (!_tables->hasSeen(pkt)) { + if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); removeSelfFromPath(pkt); uint32_t d = getDirectRetransmitDelay(pkt); @@ -117,7 +120,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; if (i > pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime()); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); onAckRecv(pkt, ack_crc); action = routeRecvPacket(pkt); } @@ -134,7 +138,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data if (i + CIPHER_MAC_SIZE >= pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); // NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins. // For flood mode, the path may not be the 'best' in terms of hops. // FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?) @@ -197,7 +202,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data if (i + 2 >= pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); if (self_id.isHashMatch(&dest_hash)) { Identity sender(sender_pub_key); @@ -224,7 +230,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data if (i + 2 >= pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM) GroupChannel channels[4]; int num = searchChannelsByHash(&channel_hash, channels, 4); @@ -255,7 +262,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime()); } else if (self_id.matches(id.pub_key)) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime()); - } else if (!_tables->hasSeen(pkt)) { + } else if (!_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); uint8_t* app_data = &pkt->payload[i]; int app_data_len = pkt->payload_len - i; if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; } @@ -282,7 +290,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { break; } case PAYLOAD_TYPE_RAW_CUSTOM: { - if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) { + if (pkt->isRouteDirect() && !_tables->wasSeen(pkt)) { + _tables->markSeen(pkt); onRawDataRecv(pkt); //action = routeRecvPacket(pkt); don't flood route these (yet) } @@ -300,7 +309,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->hasSeen(&tmp)) { + if (!_tables->wasSeen(&tmp)) { + _tables->markSeen(&tmp); uint32_t ack_crc; memcpy(&ack_crc, tmp.payload, 4); @@ -357,7 +367,8 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); - if (!_tables->hasSeen(&tmp)) { // don't retransmit! + if (!_tables->wasSeen(&tmp)) { // don't retransmit! + _tables->markSeen(&tmp); removeSelfFromPath(&tmp); routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) } @@ -637,7 +648,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si packet->header |= ROUTE_TYPE_FLOOD; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -666,7 +677,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->transport_codes[1] = transport_codes[1]; packet->setPathHashSizeAndCount(path_hash_size, 0); - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -699,7 +710,7 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 0; } } - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, pri, delay_millis); } @@ -709,7 +720,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } @@ -722,7 +733,7 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } diff --git a/src/Mesh.h b/src/Mesh.h index 932541db55..49a299a6a4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -15,8 +15,9 @@ class GroupChannel { */ class MeshTables { public: - virtual bool hasSeen(const Packet* packet) = 0; - virtual void clear(const Packet* packet) = 0; // remove this packet hash from table + virtual bool wasSeen(const Packet* packet) = 0; + virtual void markSeen(const Packet* packet) = 0; + virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; /** diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 0b79cfb422..956f36faa6 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -31,27 +31,31 @@ class SimpleMeshTables : public mesh::MeshTables { } #endif - bool hasSeen(const mesh::Packet* packet) override { + bool wasSeen(const mesh::Packet* packet) override { uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats + _direct_dups++; } else { _flood_dups++; } return true; } } - - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table return false; } + void markSeen(const mesh::Packet* packet) override { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + memcpy(&_hashes[_next_idx * MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } + void clear(const mesh::Packet* packet) override { uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); diff --git a/src/helpers/bridges/BridgeBase.cpp b/src/helpers/bridges/BridgeBase.cpp index d2e2e5e007..8093d3cb5b 100644 --- a/src/helpers/bridges/BridgeBase.cpp +++ b/src/helpers/bridges/BridgeBase.cpp @@ -39,7 +39,8 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) { return; } - if (!_seen_packets.hasSeen(packet)) { + if (!_seen_packets.wasSeen(packet)) { + _seen_packets.markSeen(packet); // bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network. _mgr->queueInbound(packet, millis() + _prefs->bridge_delay); } else { diff --git a/src/helpers/bridges/BridgeBase.h b/src/helpers/bridges/BridgeBase.h index 04c1564bd4..8bbe646678 100644 --- a/src/helpers/bridges/BridgeBase.h +++ b/src/helpers/bridges/BridgeBase.h @@ -110,7 +110,7 @@ class BridgeBase : public AbstractBridge { * @brief Common packet handling for received packets * * Implements the standard pattern used by all bridges: - * - Check if packet was seen before using _seen_packets.hasSeen() + * - Check if packet was seen before using _seen_packets.wasSeen() * - Queue packet for mesh processing if not seen before * - Free packet if already seen to prevent duplicates * diff --git a/src/helpers/bridges/ESPNowBridge.cpp b/src/helpers/bridges/ESPNowBridge.cpp index 808e9df434..3c094e7c90 100644 --- a/src/helpers/bridges/ESPNowBridge.cpp +++ b/src/helpers/bridges/ESPNowBridge.cpp @@ -167,7 +167,8 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) { return; } - if (!_seen_packets.hasSeen(packet)) { + if (!_seen_packets.wasSeen(packet)) { + _seen_packets.markSeen(packet); // Create a temporary buffer just for size calculation and reuse for actual writing uint8_t sizingBuffer[MAX_PAYLOAD_SIZE]; uint16_t meshPacketLen = packet->writeTo(sizingBuffer); diff --git a/src/helpers/bridges/RS232Bridge.cpp b/src/helpers/bridges/RS232Bridge.cpp index 0024f6f2f2..f719d342e4 100644 --- a/src/helpers/bridges/RS232Bridge.cpp +++ b/src/helpers/bridges/RS232Bridge.cpp @@ -115,7 +115,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) { return; } - if (!_seen_packets.hasSeen(packet)) { + if (!_seen_packets.wasSeen(packet)) { + _seen_packets.markSeen(packet); uint8_t buffer[MAX_SERIAL_PACKET_SIZE]; uint16_t len = packet->writeTo(buffer + 4); diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index b6e551a077..0cefe5c6e3 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -3,12 +3,33 @@ #include #include -// Mock SHA256 class for testing -// Provides minimal interface to allow Utils.cpp to compile +// Mock SHA256 for native testing — deterministic but not cryptographic. +// finalize() writes real (non-garbage) output so calculatePacketHash() produces +// distinguishable results for packets with different payloads. +#include + class SHA256 { + uint8_t _state[32]; + size_t _len; public: - void update(const uint8_t* data, size_t len) {} - void finalize(uint8_t* hash, size_t hashLen) {} + SHA256() : _len(0) { memset(_state, 0, sizeof(_state)); } + + void update(const void* data, size_t len) { + const uint8_t* bytes = static_cast(data); + for (size_t i = 0; i < len; i++) { + uint8_t b = bytes[i]; + _state[_len % 32] ^= b; + _state[(_len + 1) % 32] += (uint8_t)((b >> 1) | (b << 7)); + _len++; + } + } + + void finalize(uint8_t* hash, size_t hashLen) { + for (size_t i = 0; i < hashLen; i++) { + hash[i] = _state[i % 32]; + } + } + void resetHMAC(const uint8_t* key, size_t keyLen) {} void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {} }; diff --git a/test/test_mesh_tables/test_simple_mesh_tables.cpp b/test/test_mesh_tables/test_simple_mesh_tables.cpp new file mode 100644 index 0000000000..46b477d944 --- /dev/null +++ b/test/test_mesh_tables/test_simple_mesh_tables.cpp @@ -0,0 +1,103 @@ +#include +#include "helpers/SimpleMeshTables.h" + +using namespace mesh; + +// Build a packet that calculatePacketHash() distinguishes by payload content. +// header selects ROUTE_TYPE_FLOOD so isRouteDirect() returns false. +static Packet makeFloodPacket(uint8_t seed) { + Packet p; + p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + p.payload[0] = seed; + p.payload_len = 1; + p.path_len = 0; + return p; +} + +static Packet makeDirectPacket(uint8_t seed) { + Packet p; + p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); + p.payload[0] = seed; + p.payload_len = 1; + p.path_len = 0; + return p; +} + +// ── wasSeen: pure query ─────────────────────────────────────────────────────── + +TEST(SimpleMeshTables, WasSeen_ReturnsFalseForUnseen) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + EXPECT_FALSE(t.wasSeen(&p)); +} + +// wasSeen shouldn't change state +TEST(SimpleMeshTables, WasSeen_IsPureQuery_DoesNotInsert) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + EXPECT_FALSE(t.wasSeen(&p)); + EXPECT_FALSE(t.wasSeen(&p)); +} + +// ── markSeen + wasSeen ─────────────────────────────────────────────────────── + +TEST(SimpleMeshTables, MarkSeen_MakesWasSeenReturnTrue) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + t.markSeen(&p); + EXPECT_TRUE(t.wasSeen(&p)); +} + +TEST(SimpleMeshTables, MarkSeen_DoesNotAffectOtherPackets) { + SimpleMeshTables t; + Packet p1 = makeFloodPacket(0x01); + Packet p2 = makeFloodPacket(0x02); + t.markSeen(&p1); + EXPECT_FALSE(t.wasSeen(&p2)); +} + +// Canonical pattern used at every onRecvPacket call site: +// if (!wasSeen(pkt)) { markSeen(pkt); process(pkt); } +TEST(SimpleMeshTables, QueryThenMark_WorksCorrectly) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + EXPECT_FALSE(t.wasSeen(&p)); + t.markSeen(&p); + EXPECT_TRUE(t.wasSeen(&p)); +} + +// ── dup stats ──────────────────────────────────────────────────────────────── + +TEST(SimpleMeshTables, WasSeen_IncrementsFloodDupStat) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + t.markSeen(&p); + t.wasSeen(&p); + EXPECT_EQ(1u, t.getNumFloodDups()); + EXPECT_EQ(0u, t.getNumDirectDups()); +} + +TEST(SimpleMeshTables, WasSeen_IncrementsDirectDupStat) { + SimpleMeshTables t; + Packet p = makeDirectPacket(0x01); + t.markSeen(&p); + t.wasSeen(&p); + EXPECT_EQ(0u, t.getNumFloodDups()); + EXPECT_EQ(1u, t.getNumDirectDups()); +} + +// ── clear ──────────────────────────────────────────────────────────────────── + +TEST(SimpleMeshTables, Clear_RemovesSeenPacket) { + SimpleMeshTables t; + Packet p = makeFloodPacket(0x01); + t.markSeen(&p); + ASSERT_TRUE(t.wasSeen(&p)); + t.clear(&p); + EXPECT_FALSE(t.wasSeen(&p)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 0d7379520ff3fe92c2ad06cf8b39b3bdceb29b45 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 1 Jul 2026 15:25:34 +0100 Subject: [PATCH 54/74] Fix `recv_pkt_region` incorrect usage The `recv_pkt_region` is set when processing a flood packet in `filterRecvFloodPacket` but direct/non-flood packets would never pass through that function, so the pointer was not cleared for them. `sendFloodReply` would then later use it blindly, which meant that the response would either inherit the region from the last flood packet, or refer to a non-initialised pointer if no region floods had been received yet. --- examples/simple_repeater/MyMesh.cpp | 7 +++---- examples/simple_repeater/MyMesh.h | 2 +- examples/simple_room_server/MyMesh.cpp | 7 +++---- examples/simple_room_server/MyMesh.h | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca4cfad27d..b66e19522a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -549,8 +549,7 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } -bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { - // just try to determine region for packet (apply later in allowPacketForward()) +mesh::DispatcherAction MyMesh::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) { @@ -562,8 +561,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { } else { recv_pkt_region = NULL; } - // do normal processing - return false; + return Mesh::onRecvPacket(pkt); } void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, @@ -867,6 +865,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc set_radio_at = revert_radio_at = 0; _logging = false; region_load_active = false; + recv_pkt_region = NULL; #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index fb091a4cf6..0b2e7491b7 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -166,7 +166,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { } #endif - bool filterRecvFloodPacket(mesh::Packet* pkt) 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; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 12d0b0c318..36978e808f 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -290,8 +290,7 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) { return true; } -bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { - // just try to determine region for packet (apply later in allowPacketForward()) +mesh::DispatcherAction MyMesh::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) { @@ -303,8 +302,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { } else { recv_pkt_region = NULL; } - // do normal processing - return false; + return Mesh::onRecvPacket(pkt); } void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender, @@ -627,6 +625,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _logging = false; region_load_active = false; set_radio_at = revert_radio_at = 0; + recv_pkt_region = NULL; // defaults memset(&_prefs, 0, sizeof(_prefs)); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index e9e53ec919..6bab9dc2d0 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -154,7 +154,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { return _prefs.multi_acks; } - bool filterRecvFloodPacket(mesh::Packet* pkt) override; + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; bool allowPacketForward(const mesh::Packet* packet) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; From b40968a0f08b93cf14104e6f8d7ef45338c913ab Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 5 Jul 2026 12:23:22 +1200 Subject: [PATCH 55/74] stop tracking vscode extensions.json --- .vscode/extensions.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 8057bc70a7..0000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "pioarduino.pioarduino-ide", - "platformio.platformio-ide" - ], - "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" - ] -} From a92046b0eb73d5bc40af07c70c14fff246b196a7 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 21:20:13 +0700 Subject: [PATCH 56/74] Put powerOff and enterDeepSleep to ESP32Board --- src/helpers/ESP32Board.cpp | 42 ++++++++++++++++++++++++++++++++++++++ src/helpers/ESP32Board.h | 3 +++ 2 files changed, 45 insertions(+) diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index e0ca1d0eeb..a55abb264e 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -1,6 +1,7 @@ #ifdef ESP_PLATFORM #include "ESP32Board.h" +#include #if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only #include @@ -44,4 +45,45 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { } #endif +void ESP32Board::powerOff() { + enterDeepSleep(0); // Do not wakeup +} + +void ESP32Board::enterDeepSleep(uint32_t secs) { + // Power off the display if any +#ifdef DISPLAY_CLASS + display.turnOff(); +#endif + + // Power off LoRa + radio_driver.powerOff(); + + // Keep LoRa inactive during deepsleep + digitalWrite(P_LORA_NSS, HIGH); +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) + gpio_hold_en((gpio_num_t)P_LORA_NSS); +#else + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); +#endif + + // Power off GPS if any + if (sensors.getLocationProvider() != NULL) { + sensors.getLocationProvider()->stop(); + } + + // Flush serial buffers + Serial.flush(); + delay(100); + + // Clear stale wakeup sources to avoid ghost wakeup + // This is required when Power Management and automatic lightsleep are enabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000ULL); + } + + // Finally set ESP32 into deepsleep + esp_deep_sleep_start(); // CPU halts here and never returns! +} #endif diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 1efc99f364..45d7761b69 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -62,6 +62,9 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } + virtual void powerOff() override; + void enterDeepSleep(uint32_t secs); + uint32_t getIRQGpio() override { return P_LORA_DIO_1; // default for SX1262 } From 73b7367e4559d42f8b9ac8329a0c4d36ba0408d5 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 21:20:39 +0700 Subject: [PATCH 57/74] Put powerOff to NRF52Board --- src/helpers/NRF52Board.cpp | 32 ++++++++++++++++++++++++++++++++ src/helpers/NRF52Board.h | 1 + 2 files changed, 33 insertions(+) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 17265f0455..beee32126f 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -1,5 +1,6 @@ #if defined(NRF52_PLATFORM) #include "NRF52Board.h" +#include #include #include @@ -297,6 +298,37 @@ float NRF52Board::getMCUTemperature() { return temp * 0.25f; // Convert to *C } +void NRF52Board::powerOff() { + // Power off the display if any +#ifdef DISPLAY_CLASS + display.turnOff(); +#endif + + // Power off LoRa + radio_driver.powerOff(); + + // Keep LoRa inactive during deepsleep + digitalWrite(P_LORA_NSS, HIGH); + + // Power off GPS if any + if(sensors.getLocationProvider() != NULL) { + sensors.getLocationProvider()->stop(); + } + + // Flush serial buffers + Serial.flush(); + delay(100); + + // Enter SYSTEMOFF + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { // SoftDevice is enabled + sd_power_system_off(); + } else { // SoftDevice is not enable + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + } +} + bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) { static const char BOOTLOADER_MARKER[] = "UF2 Bootloader "; const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 17065cf443..cbf4cd498d 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ class NRF52Board : public mesh::MainBoard { virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual void powerOff() override; virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; From 7d7de8870719bce1178b9966f0c1c18ce18dba63 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 21:22:13 +0700 Subject: [PATCH 58/74] board->powerOff() is sufficient to power off radio, display, GPS and components --- examples/companion_radio/ui-new/UITask.cpp | 3 +-- examples/companion_radio/ui-orig/UITask.cpp | 2 +- examples/companion_radio/ui-tiny/UITask.cpp | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 7c84201941..403ea463fe 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -697,8 +697,7 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); + // Power off board including radio, display, GPS and components _board->powerOff(); } } diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 5529046775..b48f64121e 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -307,7 +307,7 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - radio_driver.powerOff(); + // Power off board including radio, display, GPS and components _board->powerOff(); } } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 0119475e77..452c02d41c 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -566,8 +566,7 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); + // Power off board including radio, display, GPS and components _board->powerOff(); } } From eec75eab312d8338001760a3ac17cfd7518af5e8 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 22:10:47 +0700 Subject: [PATCH 59/74] Cleanup Poweroff for NRF52 boards --- .../GAT56230SMeshKitBoard.h | 2 +- .../gat562_mesh_evb_pro/GAT562EVBProBoard.h | 2 +- .../GAT562MeshTrackerProBoard.h | 2 +- .../GAT56MeshWatch13Board.h | 2 +- variants/heltec_t096/T096Board.cpp | 10 ++----- variants/heltec_t1/T1Board.cpp | 30 +------------------ variants/heltec_t114/T114Board.h | 7 ++--- variants/keepteen_lt1/KeepteenLT1Board.h | 4 --- .../lilygo_t_impulse_plus/TImpulsePlusBoard.h | 6 ++-- variants/lilygo_techo/TechoBoard.h | 2 +- variants/lilygo_techo_card/TechoCardBoard.cpp | 2 +- variants/lilygo_techo_lite/TechoBoard.h | 3 +- variants/mesh_pocket/MeshPocket.h | 4 --- variants/meshtiny/MeshtinyBoard.h | 2 +- .../MinewsemiME25LS01Board.h | 2 +- variants/nano_g2_ultra/nano-g2.h | 2 +- variants/promicro/PromicroBoard.h | 4 --- variants/rak_wismesh_tag/RAKWismeshTagBoard.h | 2 +- variants/sensecap_solar/SenseCapSolarBoard.h | 2 +- variants/t1000-e/T1000eBoard.h | 2 +- variants/thinknode_m1/ThinkNodeM1Board.h | 3 +- variants/thinknode_m3/ThinkNodeM3Board.h | 2 +- variants/thinknode_m6/ThinkNodeM6Board.h | 2 +- variants/wio-tracker-l1/WioTrackerL1Board.h | 2 +- variants/xiao_nrf52/XiaoNrf52Board.h | 2 +- 25 files changed, 27 insertions(+), 76 deletions(-) diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h index ab7ecc2451..0b0f701c26 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h @@ -47,7 +47,7 @@ class GAT56230SMeshKitBoard : public NRF52BoardDCDC { uint32_t button_pin = PIN_BUTTON1; nrf_gpio_cfg_input(button_pin, NRF_GPIO_PIN_PULLUP); nrf_gpio_cfg_sense_set(button_pin, NRF_GPIO_PIN_SENSE_LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h index 33c3d05fc5..66c770994d 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h @@ -47,7 +47,7 @@ class GAT562EVBProBoard : public NRF52BoardDCDC { uint32_t button_pin = PIN_BUTTON1; nrf_gpio_cfg_input(button_pin, NRF_GPIO_PIN_PULLUP); nrf_gpio_cfg_sense_set(button_pin, NRF_GPIO_PIN_SENSE_LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h index aa1772e4c8..39b55911a1 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h @@ -47,7 +47,7 @@ class GAT562MeshTrackerProBoard : public NRF52BoardDCDC { uint32_t button_pin = PIN_BUTTON1; nrf_gpio_cfg_input(button_pin, NRF_GPIO_PIN_PULLUP); nrf_gpio_cfg_sense_set(button_pin, NRF_GPIO_PIN_SENSE_LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h index da792b78b1..805ca67f24 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h @@ -38,7 +38,7 @@ class GAT56MeshWatch13Board : public NRF52BoardDCDC { uint32_t button_pin = PIN_BUTTON1; nrf_gpio_cfg_input(button_pin, NRF_GPIO_PIN_PULLUP); nrf_gpio_cfg_sense_set(button_pin, NRF_GPIO_PIN_SENSE_LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 54425145c4..78af529d67 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -112,13 +112,9 @@ void T096Board::variant_shutdown() { } void T096Board::powerOff() { -#if ENV_INCLUDE_GPS == 1 - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, !PIN_GPS_EN_ACTIVE); -#endif - loRaFEMControl.setSleepModeEnable(); - variant_shutdown(); - sd_power_system_off(); + loRaFEMControl.setSleepModeEnable(); + nrf_gpio_cfg_default(PIN_GPS_EN); // 363uA down to 39uA + NRF52Board::powerOff(); } const char* T096Board::getManufacturerName() const { diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index 490f86787e..1ef5b766b3 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -81,34 +81,6 @@ uint16_t T1Board::getBattMilliVolts() { } void T1Board::variant_shutdown() { - nrf_gpio_cfg_default(PIN_TFT_CS); - nrf_gpio_cfg_default(PIN_TFT_DC); - nrf_gpio_cfg_default(PIN_TFT_SDA); - nrf_gpio_cfg_default(PIN_TFT_SCL); - nrf_gpio_cfg_default(PIN_TFT_RST); - nrf_gpio_cfg_default(PIN_TFT_LEDA_CTL); - nrf_gpio_cfg_default(PIN_TFT_VDD_CTL); - - nrf_gpio_cfg_default(PIN_WIRE_SDA); - nrf_gpio_cfg_default(PIN_WIRE_SCL); - - nrf_gpio_cfg_default(LORA_CS); - nrf_gpio_cfg_default(SX126X_DIO1); - nrf_gpio_cfg_default(SX126X_BUSY); - nrf_gpio_cfg_default(SX126X_RESET); - nrf_gpio_cfg_default(PIN_SPI_MISO); - nrf_gpio_cfg_default(PIN_SPI_MOSI); - nrf_gpio_cfg_default(PIN_SPI_SCK); - - nrf_gpio_cfg_default(PIN_SPI1_MOSI); - nrf_gpio_cfg_default(PIN_SPI1_SCK); - - nrf_gpio_cfg_default(PIN_GPS_RESET); - nrf_gpio_cfg_default(PIN_GPS_EN); - nrf_gpio_cfg_default(PIN_GPS_PPS); - nrf_gpio_cfg_default(PIN_GPS_RX); - nrf_gpio_cfg_default(PIN_GPS_TX); - nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_1); nrf_gpio_cfg_default(PIN_BUZZER_VOLTAGE_MULTIPLIER_2); @@ -127,7 +99,7 @@ void T1Board::variant_shutdown() { void T1Board::powerOff() { variant_shutdown(); - sd_power_system_off(); + NRF52Board::powerOff(); } const char* T1Board::getManufacturerName() const { diff --git a/variants/heltec_t114/T114Board.h b/variants/heltec_t114/T114Board.h index f27dc291d0..bd76d3de44 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -50,10 +50,7 @@ class T114Board : public NRF52BoardDCDC { #ifdef LED_PIN digitalWrite(LED_PIN, HIGH); #endif -#if ENV_INCLUDE_GPS == 1 - pinMode(GPS_EN, OUTPUT); - digitalWrite(GPS_EN, LOW); -#endif - sd_power_system_off(); + + NRF52Board::powerOff(); } }; diff --git a/variants/keepteen_lt1/KeepteenLT1Board.h b/variants/keepteen_lt1/KeepteenLT1Board.h index 752b27e752..25d4d86762 100644 --- a/variants/keepteen_lt1/KeepteenLT1Board.h +++ b/variants/keepteen_lt1/KeepteenLT1Board.h @@ -37,8 +37,4 @@ class KeepteenLT1Board : public NRF52Board { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off } #endif - - void powerOff() override { - sd_power_system_off(); - } }; diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index ea1782cf7a..5fafcad862 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -48,12 +48,10 @@ class TImpulsePlusBoard : public NRF52BoardDCDC { } void powerOff() override { + // power off system + NRF52Board::powerOff(); // turn off 3.3v digitalWrite(RT9080_EN, LOW); - - // power off system - sd_power_system_off(); - } }; diff --git a/variants/lilygo_techo/TechoBoard.h b/variants/lilygo_techo/TechoBoard.h index e560cd14b4..e957d2e577 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -24,6 +24,7 @@ class TechoBoard : public NRF52BoardDCDC { } void powerOff() override { + NRF52Board::powerOff(); #ifdef LED_RED digitalWrite(LED_RED, HIGH); #endif @@ -39,6 +40,5 @@ class TechoBoard : public NRF52BoardDCDC { #ifdef PIN_PWR_EN digitalWrite(PIN_PWR_EN, LOW); #endif - sd_power_system_off(); } }; diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index f0dcef312a..8143587d50 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -91,7 +91,7 @@ void TechoCardBoard::powerOff() { nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); turnOffLeds(); digitalWrite(PIN_PWR_EN, LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } #endif diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index 7a43fd8383..f4c16016b0 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -22,6 +22,8 @@ class TechoBoard : public NRF52BoardDCDC { } void powerOff() override { + NRF52Board::powerOff(); + digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED digitalWrite(LED_RED, LOW); @@ -38,6 +40,5 @@ class TechoBoard : public NRF52BoardDCDC { #ifdef PIN_PWR_EN digitalWrite(PIN_PWR_EN, LOW); #endif - sd_power_system_off(); } }; \ No newline at end of file diff --git a/variants/mesh_pocket/MeshPocket.h b/variants/mesh_pocket/MeshPocket.h index 478bd56d71..e215eb7768 100644 --- a/variants/mesh_pocket/MeshPocket.h +++ b/variants/mesh_pocket/MeshPocket.h @@ -32,8 +32,4 @@ class HeltecMeshPocket : public NRF52BoardDCDC { const char* getManufacturerName() const override { return "Heltec MeshPocket"; } - - void powerOff() override { - sd_power_system_off(); - } }; diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index b69c0e419c..67a521fe97 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -60,7 +60,7 @@ class MeshtinyBoard : public NRF52BoardDCDC { nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h index 4fa5cd41ea..450a3d13b0 100644 --- a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h @@ -65,7 +65,7 @@ class MinewsemiME25LS01Board : public NRF52BoardDCDC { #ifdef BUTTON_PIN nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif - sd_power_system_off(); + NRF52Board::powerOff(); } #if defined(P_LORA_TX_LED) diff --git a/variants/nano_g2_ultra/nano-g2.h b/variants/nano_g2_ultra/nano-g2.h index cf771efe7d..bf9543b039 100644 --- a/variants/nano_g2_ultra/nano-g2.h +++ b/variants/nano_g2_ultra/nano-g2.h @@ -52,6 +52,6 @@ class NanoG2Ultra : public NRF52Board { nrf_gpio_cfg_sense_input(digitalPinToInterrupt(PIN_USER_BTN), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/promicro/PromicroBoard.h b/variants/promicro/PromicroBoard.h index 7b6afb1b3d..b190c47c81 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -72,8 +72,4 @@ class PromicroBoard : public NRF52BoardDCDC { #endif return 0; } - - void powerOff() override { - sd_power_system_off(); - } }; diff --git a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h index cc5aa06f59..daa90d746a 100644 --- a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h +++ b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h @@ -69,6 +69,6 @@ class RAKWismeshTagBoard : public NRF52BoardDCDC { nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h index 6799a5e921..5a8861c298 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -54,7 +54,7 @@ class SenseCapSolarBoard : public NRF52BoardDCDC { #ifdef NRF52_POWER_MANAGEMENT initiateShutdown(SHUTDOWN_REASON_USER); #else - sd_power_system_off(); + NRF52Board::powerOff(); #endif } }; diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index e7653fb2f4..1111d3c408 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -88,6 +88,6 @@ class T1000eBoard : public NRF52BoardDCDC { nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_HIGH); #endif - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/thinknode_m1/ThinkNodeM1Board.h b/variants/thinknode_m1/ThinkNodeM1Board.h index ebc46e6e6f..94224d9c2b 100644 --- a/variants/thinknode_m1/ThinkNodeM1Board.h +++ b/variants/thinknode_m1/ThinkNodeM1Board.h @@ -40,7 +40,6 @@ class ThinkNodeM1Board : public NRF52Board { #endif // power off board - sd_power_system_off(); - + NRF52Board::powerOff(); } }; diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 1435d31d47..396d80d1a3 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -49,6 +49,6 @@ class ThinkNodeM3Board : public NRF52BoardDCDC { #endif // power off board - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 32baa2a0a2..78815e2c9c 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -44,6 +44,6 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { #endif // power off board - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index 052238e66f..e376e0666d 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -35,6 +35,6 @@ class WioTrackerL1Board : public NRF52BoardDCDC { } void powerOff() override { - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index 2790dbad90..b2638a4401 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -50,7 +50,7 @@ class XiaoNrf52Board : public NRF52BoardDCDC { nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); #endif - sd_power_system_off(); + NRF52Board::powerOff(); } }; From 6b205da4e9935266b23c30a794045e1ebe4688c2 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 23:33:56 +0700 Subject: [PATCH 60/74] Added missing driver/rtc_io.h --- src/helpers/ESP32Board.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 45d7761b69..d7eb5fee26 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -14,6 +14,7 @@ #include #include "soc/rtc.h" #include "esp_system.h" +#include class ESP32Board : public mesh::MainBoard { protected: From 35f654ced36a39152606a2aa77d7c95c90e457cd Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 5 Jul 2026 23:47:27 +0700 Subject: [PATCH 61/74] Fixed hibernate/powerOff for ESP32 boards to stay at uA --- src/helpers/MeshadventurerBoard.h | 30 ----------- variants/heltec_e213/HeltecE213Board.cpp | 27 ---------- variants/heltec_e213/HeltecE213Board.h | 3 -- variants/heltec_e290/HeltecE290Board.cpp | 27 ---------- variants/heltec_e290/HeltecE290Board.h | 3 -- variants/heltec_t190/HeltecT190Board.cpp | 27 ---------- variants/heltec_t190/HeltecT190Board.h | 3 -- .../HeltecTrackerV2Board.cpp | 31 ++--------- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 2 - variants/heltec_v2/HeltecV2Board.h | 25 --------- variants/heltec_v3/HeltecV3Board.h | 29 ----------- variants/heltec_v4/HeltecV4Board.cpp | 31 ++--------- variants/heltec_v4/HeltecV4Board.h | 2 - variants/lilygo_tdeck/TDeckBoard.h | 24 --------- variants/rak3112/RAK3112Board.h | 29 ----------- variants/station_g2/StationG2Board.h | 24 --------- variants/station_g3_esp32/StationG3Board.cpp | 15 ++++++ variants/station_g3_esp32/StationG3Board.h | 31 +---------- variants/thinknode_m2/ThinknodeM2Board.cpp | 52 ++++++++----------- variants/thinknode_m2/ThinknodeM2Board.h | 4 -- variants/thinknode_m5/ThinknodeM5Board.cpp | 8 --- variants/thinknode_m5/ThinknodeM5Board.h | 3 -- variants/xiao_c3/XiaoC3Board.h | 32 ------------ 23 files changed, 47 insertions(+), 415 deletions(-) create mode 100644 variants/station_g3_esp32/StationG3Board.cpp diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 65e1110294..0325161d5d 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/src/helpers/MeshadventurerBoard.h @@ -15,8 +15,6 @@ #include "ESP32Board.h" -#include - class MeshadventurerBoard : public ESP32Board { public: @@ -35,34 +33,6 @@ class MeshadventurerBoard : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - // TODO: re-enable this when there is a definite wake-up source pin: - // enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/heltec_e213/HeltecE213Board.cpp b/variants/heltec_e213/HeltecE213Board.cpp index af11531821..88737c4d53 100644 --- a/variants/heltec_e213/HeltecE213Board.cpp +++ b/variants/heltec_e213/HeltecE213Board.cpp @@ -20,33 +20,6 @@ void HeltecE213Board::begin() { } } - void HeltecE213Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecE213Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecE213Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e213/HeltecE213Board.h b/variants/heltec_e213/HeltecE213Board.h index 2192c14104..fadc038f94 100644 --- a/variants/heltec_e213/HeltecE213Board.h +++ b/variants/heltec_e213/HeltecE213Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecE213Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecE213Board : public ESP32Board { HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; }; diff --git a/variants/heltec_e290/HeltecE290Board.cpp b/variants/heltec_e290/HeltecE290Board.cpp index 3994a20616..96ec59c9bd 100644 --- a/variants/heltec_e290/HeltecE290Board.cpp +++ b/variants/heltec_e290/HeltecE290Board.cpp @@ -20,33 +20,6 @@ void HeltecE290Board::begin() { } } - void HeltecE290Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecE290Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecE290Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e290/HeltecE290Board.h b/variants/heltec_e290/HeltecE290Board.h index 645ec3481a..f287227c6e 100644 --- a/variants/heltec_e290/HeltecE290Board.h +++ b/variants/heltec_e290/HeltecE290Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecE290Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecE290Board : public ESP32Board { HeltecE290Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_t190/HeltecT190Board.cpp b/variants/heltec_t190/HeltecT190Board.cpp index 4f35be400b..0a16b52b8d 100644 --- a/variants/heltec_t190/HeltecT190Board.cpp +++ b/variants/heltec_t190/HeltecT190Board.cpp @@ -20,33 +20,6 @@ void HeltecT190Board::begin() { } } - void HeltecT190Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecT190Board::powerOff() { - enterDeepSleep(0); - } - uint16_t HeltecT190Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_t190/HeltecT190Board.h b/variants/heltec_t190/HeltecT190Board.h index bc38c1e090..557c070ecd 100644 --- a/variants/heltec_t190/HeltecT190Board.h +++ b/variants/heltec_t190/HeltecT190Board.h @@ -3,7 +3,6 @@ #include #include #include -#include class HeltecT190Board : public ESP32Board { @@ -13,8 +12,6 @@ class HeltecT190Board : public ESP32Board { HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index f182c905e6..99b1cdfe08 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -35,33 +35,12 @@ void HeltecTrackerV2Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecTrackerV2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + void HeltecTrackerV2Board::powerOff() { + // Turn off PA + digitalWrite(P_LORA_PA_POWER, LOW); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecTrackerV2Board::powerOff() { - enterDeepSleep(0); + ESP32Board::powerOff(); } uint16_t HeltecTrackerV2Board::getBattMilliVolts() { diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index ccbecc7ab6..2bd6a02544 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { @@ -17,7 +16,6 @@ class HeltecTrackerV2Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index fe800890b8..9b08fe9430 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -7,8 +7,6 @@ #define PIN_VBAT_READ 37 #define PIN_LED_BUILTIN 25 -#include - class HeltecV2Board : public ESP32Board { public: void begin() { @@ -26,29 +24,6 @@ class HeltecV2Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() override { analogReadResolution(10); diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index ba22a7f2b9..7e7abe313b 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -17,8 +17,6 @@ #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH -#include - class HeltecV3Board : public ESP32Board { private: bool adc_active_state; @@ -52,33 +50,6 @@ class HeltecV3Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, adc_active_state); diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3978c51ea6..3f13f41f6e 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -32,33 +32,12 @@ void HeltecV4Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecV4Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + void HeltecV4Board::powerOff() { + // Turn off PA + digitalWrite(P_LORA_PA_POWER, LOW); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void HeltecV4Board::powerOff() { - enterDeepSleep(0); + ESP32Board::powerOff(); } uint16_t HeltecV4Board::getBattMilliVolts() { diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index fc37b9f6ae..55166bb37f 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "LoRaFEMControl.h" #ifndef ADC_MULTIPLIER @@ -23,7 +22,6 @@ class HeltecV4Board : public ESP32Board { void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index 7ed007af9c..e2844360df 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -3,7 +3,6 @@ #include #include #include "helpers/ESP32Board.h" -#include #define PIN_VBAT_READ 4 #define BATTERY_SAMPLES 8 @@ -23,29 +22,6 @@ class TDeckBoard : public ESP32Board { } #endif - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h index 8ba3197cf6..704162b8c8 100644 --- a/variants/rak3112/RAK3112Board.h +++ b/variants/rak3112/RAK3112Board.h @@ -16,8 +16,6 @@ #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) #define BATTERY_SAMPLES 8 -#include - class RAK3112Board : public ESP32Board { private: bool adc_active_state; @@ -51,33 +49,6 @@ class RAK3112Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - - void powerOff() override { - enterDeepSleep(0); - } - uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/station_g2/StationG2Board.h b/variants/station_g2/StationG2Board.h index a905682c8d..d1989ee0ff 100644 --- a/variants/station_g2/StationG2Board.h +++ b/variants/station_g2/StationG2Board.h @@ -2,7 +2,6 @@ #include #include -#include class StationG2Board : public ESP32Board { public: @@ -21,29 +20,6 @@ class StationG2Board : public ESP32Board { } } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp new file mode 100644 index 0000000000..4a49831107 --- /dev/null +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -0,0 +1,15 @@ +#include "StationG3Board.h" + +void StationG3Board::powerOff() { +#ifdef P_PA1_EN + setPAModeHigh(false); + rtc_gpio_hold_en((gpio_num_t)P_PA1_EN); +#endif + +#ifdef P_PRIMARY_LNA_EN + setPrimaryLNAControl(true); + rtc_gpio_hold_en((gpio_num_t)P_PRIMARY_LNA_EN); +#endif + + ESP32Board::powerOff(); +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index dc440d95f5..4b1fb81c10 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -73,36 +73,7 @@ class StationG3Board : public ESP32Board { setPrimaryLNAControl(true); } - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - -#ifdef P_PA1_EN - setPAModeHigh(false); - rtc_gpio_hold_en((gpio_num_t)P_PA1_EN); -#endif - -#ifdef P_PRIMARY_LNA_EN - setPrimaryLNAControl(true); - rtc_gpio_hold_en((gpio_num_t)P_PRIMARY_LNA_EN); -#endif - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); - } else { - esp_sleep_enable_ext1_wakeup((1ULL << P_LORA_DIO_1) | (1ULL << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - esp_deep_sleep_start(); - } + void powerOff() override; uint16_t getBattMilliVolts() override { return 0; diff --git a/variants/thinknode_m2/ThinknodeM2Board.cpp b/variants/thinknode_m2/ThinknodeM2Board.cpp index 0596510361..8d68006d2c 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.cpp +++ b/variants/thinknode_m2/ThinknodeM2Board.cpp @@ -1,40 +1,30 @@ #include "ThinknodeM2Board.h" - - void ThinknodeM2Board::begin() { - pinMode(PIN_VEXT_EN, OUTPUT); - digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle - delay(20); // allow power rail to discharge - digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on - delay(120); // give display time to bias on cold boot - ESP32Board::begin(); - pinMode(PIN_STATUS_LED, OUTPUT); // init power led - } + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle + delay(20); // allow power rail to discharge + digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on + delay(120); // give display time to bias on cold boot + ESP32Board::begin(); + pinMode(PIN_STATUS_LED, OUTPUT); // init power led +} - void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_deep_sleep_start(); - } +uint16_t ThinknodeM2Board::getBattMilliVolts() { + analogReadResolution(12); + analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); - void ThinknodeM2Board::powerOff() { - enterDeepSleep(0); + uint32_t mv = 0; + for (int i = 0; i < 8; ++i) { + mv += analogReadMilliVolts(PIN_VBAT_READ); + delayMicroseconds(200); } + mv /= 8; - uint16_t ThinknodeM2Board::getBattMilliVolts() { - analogReadResolution(12); - analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); - - uint32_t mv = 0; - for (int i = 0; i < 8; ++i) { - mv += analogReadMilliVolts(PIN_VBAT_READ); - delayMicroseconds(200); - } - mv /= 8; - - analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER ); + analogReadResolution(10); + return static_cast(mv * ADC_MULTIPLIER); } - const char* ThinknodeM2Board::getManufacturerName() const { - return "Elecrow ThinkNode M2"; - } +const char *ThinknodeM2Board::getManufacturerName() const { + return "Elecrow ThinkNode M2"; +} diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 8011fae67d..0255677784 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.h +++ b/variants/thinknode_m2/ThinknodeM2Board.h @@ -3,15 +3,11 @@ #include #include #include -#include class ThinknodeM2Board : public ESP32Board { public: - void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m5/ThinknodeM5Board.cpp b/variants/thinknode_m5/ThinknodeM5Board.cpp index c4de538c56..2cb138e639 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.cpp +++ b/variants/thinknode_m5/ThinknodeM5Board.cpp @@ -19,14 +19,6 @@ void ThinknodeM5Board::begin() { ESP32Board::begin(); } - void ThinknodeM5Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_deep_sleep_start(); - } - - void ThinknodeM5Board::powerOff() { - enterDeepSleep(0); - } - uint16_t ThinknodeM5Board::getBattMilliVolts() { analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index 3c120027b7..57ff0d00eb 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -3,7 +3,6 @@ #include #include #include -#include #include extern PCA9557 expander; @@ -13,8 +12,6 @@ class ThinknodeM5Board : public ESP32Board { public: void begin(); - void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); - void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index 6ea1c15ffc..c5700c6884 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -3,7 +3,6 @@ #include #include -#include #include class XiaoC3Board : public ESP32Board { @@ -40,37 +39,6 @@ class XiaoC3Board : public ESP32Board { #endif } - void enterDeepSleep(uint32_t secs, int8_t wake_pin = -1) { - gpio_set_direction(gpio_num_t(P_LORA_DIO_1), GPIO_MODE_INPUT); - if (wake_pin >= 0) { - gpio_set_direction((gpio_num_t)wake_pin, GPIO_MODE_INPUT); - } - - //hold disable, isolate and power domain config functions may be unnecessary - //gpio_deep_sleep_hold_dis(); - //esp_sleep_config_gpio_isolate(); - gpio_deep_sleep_hold_en(); - -#if defined(LORA_TX_BOOST_PIN) - gpio_hold_en((gpio_num_t) LORA_TX_BOOST_PIN); - gpio_deep_sleep_hold_en(); -#endif - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - if (wake_pin >= 0) { - esp_deep_sleep_enable_gpio_wakeup((1 << P_LORA_DIO_1) | (1 << wake_pin), ESP_GPIO_WAKEUP_GPIO_HIGH); - } else { - esp_deep_sleep_enable_gpio_wakeup(1 << P_LORA_DIO_1, ESP_GPIO_WAKEUP_GPIO_HIGH); - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - #if defined(LORA_TX_BOOST_PIN) || defined(P_LORA_TX_LED) void onBeforeTransmit() override { #if defined(P_LORA_TX_LED) From 3ee2f778771a14d1606eb711b8030033cfca4a6f Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 6 Jul 2026 08:46:57 -0400 Subject: [PATCH 62/74] restore display and radio poweroff in ui --- examples/companion_radio/ui-new/UITask.cpp | 3 +++ examples/companion_radio/ui-orig/UITask.cpp | 2 ++ examples/companion_radio/ui-tiny/UITask.cpp | 2 ++ 3 files changed, 7 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 403ea463fe..28591cc1a5 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -697,6 +697,9 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + // still necessary until all boards are refactored to use poweroff + _display->turnOff(); + radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b48f64121e..34a7342bba 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -307,6 +307,8 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + _display->turnOff(); + radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 452c02d41c..a6cbe9de04 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -566,6 +566,8 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + _display->turnOff(); + radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } From fec88e13009fe215e1bebcc09a6c85196ff6ac0c Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 6 Jul 2026 12:04:42 -0400 Subject: [PATCH 63/74] fix tenstar c3 repeater build --- variants/tenstar_c3/target.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/tenstar_c3/target.h b/variants/tenstar_c3/target.h index b3ee6d177f..7839fa973a 100644 --- a/variants/tenstar_c3/target.h +++ b/variants/tenstar_c3/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include <../variants/xiao_c3/XiaoC3Board.h> #include #include #include From 4605d3b6ce7a89806a35acad9359c0caf7d2a27e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 9 Jul 2026 16:31:28 +0200 Subject: [PATCH 64/74] fix misspelled RF switch pin macros in SX1268/LLCC68/SX1276 radio wrappers --- src/helpers/radiolib/CustomLLCC68.h | 8 ++++---- src/helpers/radiolib/CustomSX1268.h | 8 ++++---- src/helpers/radiolib/CustomSX1276.h | 12 ++++++------ variants/lilygo_t3s3_sx1276/platformio.ini | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index 82f886c324..686b09ecd0 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -66,11 +66,11 @@ class CustomLLCC68 : public LLCC68 { setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); #endif #if defined(SX126X_RXEN) || defined(SX126X_TXEN) - #ifndef SX1262X_RXEN - #define SX1262X_RXEN RADIOLIB_NC + #ifndef SX126X_RXEN + #define SX126X_RXEN RADIOLIB_NC #endif - #ifndef SX1262X_TXEN - #define SX1262X_TXEN RADIOLIB_NC + #ifndef SX126X_TXEN + #define SX126X_TXEN RADIOLIB_NC #endif setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); #endif diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index cc541e49f3..0c6f828b0d 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -66,11 +66,11 @@ class CustomSX1268 : public SX1268 { setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); #endif #if defined(SX126X_RXEN) || defined(SX126X_TXEN) - #ifndef SX1262X_RXEN - #define SX1262X_RXEN RADIOLIB_NC + #ifndef SX126X_RXEN + #define SX126X_RXEN RADIOLIB_NC #endif - #ifndef SX1262X_TXEN - #define SX1262X_TXEN RADIOLIB_NC + #ifndef SX126X_TXEN + #define SX126X_TXEN RADIOLIB_NC #endif setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); #endif diff --git a/src/helpers/radiolib/CustomSX1276.h b/src/helpers/radiolib/CustomSX1276.h index bee2527431..e6f3270be1 100644 --- a/src/helpers/radiolib/CustomSX1276.h +++ b/src/helpers/radiolib/CustomSX1276.h @@ -50,14 +50,14 @@ class CustomSX1276 : public SX1276 { setCurrentLimit(SX127X_CURRENT_LIMIT); #endif - #if defined(SX176X_RXEN) || defined(SX176X_TXEN) - #ifndef SX176X_RXEN - #define SX176X_RXEN RADIOLIB_NC + #if defined(SX127X_RXEN) || defined(SX127X_TXEN) + #ifndef SX127X_RXEN + #define SX127X_RXEN RADIOLIB_NC #endif - #ifndef SX176X_TXEN - #define SX176X_TXEN RADIOLIB_NC + #ifndef SX127X_TXEN + #define SX127X_TXEN RADIOLIB_NC #endif - setRfSwitchPins(SX176X_RXEN, SX176X_TXEN); + setRfSwitchPins(SX127X_RXEN, SX127X_TXEN); #endif setCRC(1); diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index 5df632020e..e579e91ca3 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -21,8 +21,8 @@ build_flags = -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper -D SX127X_CURRENT_LIMIT=120 - -D SX176X_RXEN=21 - -D SX176X_TXEN=10 + -D SX127X_RXEN=21 + -D SX127X_TXEN=10 -D LORA_TX_POWER=20 build_src_filter = ${esp32_base.build_src_filter} +<../variants/lilygo_t3s3_sx1276> From 8ef369058df018e5c41d7fea6a438f6ec180254d Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 9 Jul 2026 19:59:05 -0700 Subject: [PATCH 65/74] fix(RegionMap): distinguish clean EOF from partial read in load() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously load() returned true unconditionally on file-open success, masking truncated or corrupt /regions2 files. Additionally, the first field of each entry record (r->id) used the same success-chaining pattern as subsequent fields, so a clean EOF at a record boundary set success=false and would have been indistinguishable from real corruption once the return value was fixed. The r->id read is now split out: n==0 is a clean EOF (break, success retains its prior value from the header read), n!=sizeof(r->id) is a partial read or corruption (break, success=false). load() now returns success instead of an unconditional true, so its return value reflects the actual parse outcome. Companion fix to #2372, which fixed the same return-true hardcoding in save(). (#1891 originally reported this on load() but was closed when #2372 landed — that PR only touched save(); this addresses the load() side.) --- src/helpers/RegionMap.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 36c1d19d59..4667e0038e 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -93,13 +93,15 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { while (num_regions < MAX_REGION_ENTRIES) { auto r = ®ions[num_regions]; - success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); + int n = file.read((uint8_t *) &r->id, sizeof(r->id)); + if (n == 0) break; // clean EOF + success = (n == sizeof(r->id)); success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); success = success && file.read(pad, sizeof(pad)) == sizeof(pad); - if (!success) break; // EOF + if (!success) break; // partial read or corruption if (r->id >= next_id) { // make sure next_id is valid next_id = r->id + 1; @@ -108,7 +110,7 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { } } file.close(); - return true; + return success; } } return false; // failed From 47e1ce5bae10f4e1f54fcdf4bbfecec7a31fc8ac Mon Sep 17 00:00:00 2001 From: Alex Beal Date: Sun, 12 Jul 2026 15:50:43 -0600 Subject: [PATCH 66/74] Update #defines External watchdog support was added (but never merged) for MeshTower V1. The update's V2's #defines to follow the conventions set there and piggy back off the support already written for V1. --- variants/heltec_tower_v2/variant.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/variants/heltec_tower_v2/variant.h b/variants/heltec_tower_v2/variant.h index 352184b866..3525d9b49c 100644 --- a/variants/heltec_tower_v2/variant.h +++ b/variants/heltec_tower_v2/variant.h @@ -82,10 +82,10 @@ #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) -#define HAS_HARDWARE_WATCHDOG -#define HARDWARE_WATCHDOG_DONE (0 + 9) -#define HARDWARE_WATCHDOG_WAKE (0 + 10) -#define HARDWARE_WATCHDOG_TIMEOUT_MS (8 * 60 * 1000) +#define HAS_EXTERNAL_WATCHDOG +#define EXTERNAL_WATCHDOG_DONE_PIN (0 + 9) +#define EXTERNAL_WATCHDOG_WAKE_PIN (0 + 10) +#define EXTERNAL_WATCHDOG_FEED_INTERNAL_MS (8 * 60 * 1000) #define SERIAL_PRINT_PORT 0 From 6343d8d96e0dfdf940c76d7f740e267be5466e62 Mon Sep 17 00:00:00 2001 From: Jody Bentley Date: Sun, 12 Jul 2026 17:54:14 -0400 Subject: [PATCH 67/74] ThinkNode M6 repeater: disable leftover MESH_DEBUG/GPS_NMEA_DEBUG The M6 is the only board whose simple_repeater env ships with MESH_DEBUG=1 and GPS_NMEA_DEBUG=1 enabled. MESH_DEBUG=1 adds a 5-second boot delay (examples/simple_repeater/main.cpp) plus verbose per-packet serial prints on the hot RX/TX path; GPS_NMEA_DEBUG=1 echoes every GPS UART character to the serial console (MicroNMEALocationProvider.h). Every sibling repeater env (M1, M3, t1000-e, RAK, Heltec) ships these off, and the M6 room_server env in this same file already has them commented. Comment them out to match. --- variants/thinknode_m6/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 6fe9043668..035476e70a 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -48,8 +48,8 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 - -D GPS_NMEA_DEBUG=1 +; -D MESH_DEBUG=1 +; -D GPS_NMEA_DEBUG=1 build_src_filter = ${ThinkNode_M6.build_src_filter} +<../examples/simple_repeater/*.cpp> lib_deps = From eb97a375d3a804a12ec5d2e2cd85b69da28810bf Mon Sep 17 00:00:00 2001 From: Alex Beal Date: Sun, 12 Jul 2026 16:17:23 -0600 Subject: [PATCH 68/74] Add an instance of ExternalWatchdogManager to Tower V2 --- variants/heltec_tower_v2/target.cpp | 33 +++++++++++++++++++++++++++++ variants/heltec_tower_v2/target.h | 11 ++++++++++ variants/heltec_tower_v2/variant.h | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/variants/heltec_tower_v2/target.cpp b/variants/heltec_tower_v2/target.cpp index ad45735487..ec8e7a17da 100644 --- a/variants/heltec_tower_v2/target.cpp +++ b/variants/heltec_tower_v2/target.cpp @@ -14,6 +14,7 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +TowerV2ExternalWatchdog external_watchdog; #ifdef DISPLAY_CLASS DISPLAY_CLASS display; @@ -29,3 +30,35 @@ mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); } + +bool TowerV2ExternalWatchdog::begin() { + last_feed_watchdog = 0; + pinMode(EXTERNAL_WATCHDOG_WAKE_PIN, INPUT); + pinMode(EXTERNAL_WATCHDOG_DONE_PIN, OUTPUT); + delay(1); + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, LOW); + delay(1); + feed(); + return true; +} + +void TowerV2ExternalWatchdog::loop() { + if (millis() - last_feed_watchdog >= EXTERNAL_WATCHDOG_FEED_INTERVAL_MS) { + feed(); + } +} + +unsigned long TowerV2ExternalWatchdog::getIntervalMs() const { + unsigned long elapsed_ms = millis() - last_feed_watchdog; + if (elapsed_ms >= EXTERNAL_WATCHDOG_FEED_INTERVAL_MS) { + return 0; + } + return EXTERNAL_WATCHDOG_FEED_INTERVAL_MS - elapsed_ms; +} + +void TowerV2ExternalWatchdog::feed() { + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, HIGH); + delay(1); + digitalWrite(EXTERNAL_WATCHDOG_DONE_PIN, LOW); + last_feed_watchdog = millis(); +} diff --git a/variants/heltec_tower_v2/target.h b/variants/heltec_tower_v2/target.h index 0371924614..0b9de40ca4 100644 --- a/variants/heltec_tower_v2/target.h +++ b/variants/heltec_tower_v2/target.h @@ -8,16 +8,27 @@ #include #include #include +#include #ifdef DISPLAY_CLASS #include #include "helpers/ui/NullDisplayDriver.h" #endif +class TowerV2ExternalWatchdog : public ExternalWatchdogManager { +public: + TowerV2ExternalWatchdog() {} + bool begin() override; + void loop() override; + unsigned long getIntervalMs() const override; + void feed() override; +}; + extern HeltecTowerV2Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; +extern TowerV2ExternalWatchdog external_watchdog; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; diff --git a/variants/heltec_tower_v2/variant.h b/variants/heltec_tower_v2/variant.h index 3525d9b49c..14116c6f34 100644 --- a/variants/heltec_tower_v2/variant.h +++ b/variants/heltec_tower_v2/variant.h @@ -85,7 +85,7 @@ #define HAS_EXTERNAL_WATCHDOG #define EXTERNAL_WATCHDOG_DONE_PIN (0 + 9) #define EXTERNAL_WATCHDOG_WAKE_PIN (0 + 10) -#define EXTERNAL_WATCHDOG_FEED_INTERNAL_MS (8 * 60 * 1000) +#define EXTERNAL_WATCHDOG_FEED_INTERVAL_MS (8 * 60 * 1000) #define SERIAL_PRINT_PORT 0 From 63731d3fbf56f798f2289ee3c91eef7ecf1bfe6e Mon Sep 17 00:00:00 2001 From: Jody Bentley Date: Sun, 12 Jul 2026 18:21:53 -0400 Subject: [PATCH 69/74] sensors: fix millis() rollover stall in GPS time-sync + minor cleanups next_check and next_gps_update stored a future millis() value in a signed long and compared with a naive '>'. After the ~24.8-day millis() sign flip the deadline sits above the wrapped millis(), so the block never runs again and GPS->RTC time-sync (and the location cache refresh) stall permanently until reboot. Switch to unsigned deadlines with the wrap-safe signed- difference compare '(long)(millis() - deadline) > 0', matching the idiom in Dispatcher::millisHasNowPassed. Also: reorder the MicroNMEALocationProvider ctor init-list to declaration order (silences -Wreorder) and drop the always-true 'if (_claims > 0)' guard in claim() (claim() always runs after _claims++, so it is >= 1). --- src/helpers/sensors/EnvironmentSensorManager.cpp | 4 ++-- src/helpers/sensors/MicroNMEALocationProvider.h | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 73842d9eeb..af09eff624 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -889,11 +889,11 @@ void EnvironmentSensorManager::stop_gps() { void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS - static long next_gps_update = 0; + static unsigned long next_gps_update = 0; if (gps_active) { _location->loop(); } - if (millis() > next_gps_update) { + if ((long)(millis() - next_gps_update) > 0) { if(gps_active){ #ifdef RAK_WISBLOCK_GPS diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3aa..6b06259b96 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -42,14 +42,14 @@ class MicroNMEALocationProvider : public LocationProvider { int8_t _claims = 0; int _pin_reset; int _pin_en; - long next_check = 0; + unsigned long next_check = 0; long time_valid = 0; unsigned long _last_time_sync = 0; static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : - _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) { + nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) { if (_pin_reset != -1) { pinMode(_pin_reset, OUTPUT); digitalWrite(_pin_reset, GPS_RESET_FORCE); @@ -62,9 +62,7 @@ public : void claim() { _claims++; - if (_claims > 0) { - if (_peripher_power) _peripher_power->claim(); - } + if (_peripher_power) _peripher_power->claim(); } void release() { @@ -143,7 +141,7 @@ public : if (!isValid()) time_valid = 0; - if (millis() > next_check) { + if ((long)(millis() - next_check) > 0) { next_check = millis() + 1000; // Re-enable time sync periodically when GPS has valid fix if (!_time_sync_needed && _clock != NULL && (millis() - _last_time_sync) > TIME_SYNC_INTERVAL) { From 1429d75f57efa3e7cf4e7228e95fcbeacfd74695 Mon Sep 17 00:00:00 2001 From: Jody Bentley Date: Sun, 12 Jul 2026 18:32:09 -0400 Subject: [PATCH 70/74] ThinkNode M6/M1 room_server: set ROOM_PASSWORD default Without ROOM_PASSWORD the guest password defaults to empty, and a client sending a blank password is granted read+write access (can post). Every other board's room_server env sets ROOM_PASSWORD; the ThinkNode M6 and M1 were the only two shipping an open room. Default them to "hello" to match the rest of the tree; operators can still change it at runtime with 'set guest.password'. --- variants/thinknode_m1/platformio.ini | 1 + variants/thinknode_m6/platformio.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 356edfee69..617f92405c 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -58,6 +58,7 @@ build_flags = -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_M1.build_src_filter} diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 6fe9043668..012afe469d 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -63,6 +63,7 @@ build_flags = -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_M6.build_src_filter} From 17b6662aa58d740667f08e0c0690bd586906c88d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 13 Jul 2026 14:39:54 +1200 Subject: [PATCH 71/74] update radiolib to v7.7.1-6d893483 --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 47cc0ab8c3..f3ada13386 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,8 @@ monitor_speed = 115200 lib_deps = SPI Wire - jgromes/RadioLib @ ^7.6.0 + ;jgromes/RadioLib @ ^7.7.1 + https://github.com/jgromes/RadioLib.git#6d8934836678d8894e3d556550475b37dce3e2b6 rweather/Crypto @ ^0.4.0 adafruit/RTClib @ ^2.1.3 melopero/Melopero RV3028 @ ^1.1.0 From 87bf371cd3f6081078af2107f0b46da39253f806 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 13 Jul 2026 17:40:39 +1200 Subject: [PATCH 72/74] initial support for thinknode m9 --- boards/thinknode_m9.json | 57 ++++++++ src/helpers/AutoDiscoverRTCClock.cpp | 1 + src/helpers/ui/ST7789Display.cpp | 14 ++ src/helpers/ui/ST7789Display.h | 4 +- variants/thinknode_m9/ThinkNodeM9Board.cpp | 23 +++ variants/thinknode_m9/ThinkNodeM9Board.h | 13 ++ variants/thinknode_m9/pins_arduino.h | 64 +++++++++ variants/thinknode_m9/platformio.ini | 160 +++++++++++++++++++++ variants/thinknode_m9/target.cpp | 78 ++++++++++ variants/thinknode_m9/target.h | 29 ++++ variants/thinknode_m9/variant.cpp | 7 + variants/thinknode_m9/variant.h | 114 +++++++++++++++ 12 files changed, 563 insertions(+), 1 deletion(-) create mode 100755 boards/thinknode_m9.json create mode 100644 variants/thinknode_m9/ThinkNodeM9Board.cpp create mode 100644 variants/thinknode_m9/ThinkNodeM9Board.h create mode 100755 variants/thinknode_m9/pins_arduino.h create mode 100755 variants/thinknode_m9/platformio.ini create mode 100644 variants/thinknode_m9/target.cpp create mode 100644 variants/thinknode_m9/target.h create mode 100644 variants/thinknode_m9/variant.cpp create mode 100755 variants/thinknode_m9/variant.h diff --git a/boards/thinknode_m9.json b/boards/thinknode_m9.json new file mode 100755 index 0000000000..31ebdbb91a --- /dev/null +++ b/boards/thinknode_m9.json @@ -0,0 +1,57 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "qio_opi", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "ELECROW-ThinkNode-M9" + }, + "connectivity": [ + "wifi", + "bluetooth", + "lora" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "elecrow-thinknode-m9", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 524288, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/src/helpers/AutoDiscoverRTCClock.cpp b/src/helpers/AutoDiscoverRTCClock.cpp index a310539bc7..af08fddb03 100644 --- a/src/helpers/AutoDiscoverRTCClock.cpp +++ b/src/helpers/AutoDiscoverRTCClock.cpp @@ -42,6 +42,7 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) { } if (i2c_probe(wire, PCF8563_ADDRESS)) { + MESH_DEBUG_PRINTLN("PCF8563: Found"); rtc_8563_success = rtc_8563.begin(&wire); } diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index f7d20b8ab7..98d6939584 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -18,6 +18,14 @@ #define SCALE_Y 2.109375f // 135 / 64 #endif +#ifdef DISPLAY_SCALE_X + #define SCALE_X DISPLAY_SCALE_X +#endif + +#ifdef DISPLAY_SCALE_Y + #define SCALE_Y DISPLAY_SCALE_Y +#endif + bool ST7789Display::begin() { if(!_isOn) { pinMode(PIN_TFT_VDD_CTL, OUTPUT); @@ -32,6 +40,9 @@ bool ST7789Display::begin() { display.init(); display.landscapeScreen(); + #ifdef DISPLAY_FLIP_VERTICALLY + display.flipScreenVertically(); + #endif display.displayOn(); setCursor(0,0); @@ -49,6 +60,9 @@ void ST7789Display::turnOn() { // Re-initialize the display display.init(); display.displayOn(); + #ifdef DISPLAY_FLIP_VERTICALLY + display.flipScreenVertically(); + #endif delay(20); // Now turn on the backlight diff --git a/src/helpers/ui/ST7789Display.h b/src/helpers/ui/ST7789Display.h index cb56ff8a79..9822a67df9 100644 --- a/src/helpers/ui/ST7789Display.h +++ b/src/helpers/ui/ST7789Display.h @@ -14,8 +14,10 @@ class ST7789Display : public DisplayDriver { bool i2c_probe(TwoWire& wire, uint8_t addr); public: -#ifdef HELTEC_VISION_MASTER_T190 +#if defined(HELTEC_VISION_MASTER_T190) ST7789Display() : DisplayDriver(128, 64), display(&SPI, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 320, 170,PIN_TFT_SDA,-1,PIN_TFT_SCL) {_isOn = false;} +#elif defined(THINKNODE_M9) + ST7789Display() : DisplayDriver(128, 64), display(&SPI, ST7789_RESET, ST7789_RS, ST7789_CS, GEOMETRY_RAWMODE, 320, 240, ST7789_SDA, ST7789_MISO, ST7789_SCK) {_isOn = false;} #else ST7789Display() : DisplayDriver(128, 64), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;} #endif diff --git a/variants/thinknode_m9/ThinkNodeM9Board.cpp b/variants/thinknode_m9/ThinkNodeM9Board.cpp new file mode 100644 index 0000000000..bf558639dd --- /dev/null +++ b/variants/thinknode_m9/ThinkNodeM9Board.cpp @@ -0,0 +1,23 @@ +#include "ThinkNodeM9Board.h" + +void ThinkNodeM9Board::begin() { + + // power on screen + pinMode(VEXT_ENABLE, OUTPUT); + digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); + + ESP32Board::begin(); + +} + +void ThinkNodeM9Board::powerOff() { + enterDeepSleep(0); +} + +uint32_t ThinkNodeM9Board::getIRQGpio() { + return LORA_DIO0; +} + +const char* ThinkNodeM9Board::getManufacturerName() const { + return "Elecrow ThinkNode M9"; +} diff --git a/variants/thinknode_m9/ThinkNodeM9Board.h b/variants/thinknode_m9/ThinkNodeM9Board.h new file mode 100644 index 0000000000..d5f68e0556 --- /dev/null +++ b/variants/thinknode_m9/ThinkNodeM9Board.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class ThinkNodeM9Board : public ESP32Board { + +public: + void begin(); + void powerOff() override; + const char* getManufacturerName() const override; + uint32_t getIRQGpio() override; +}; diff --git a/variants/thinknode_m9/pins_arduino.h b/variants/thinknode_m9/pins_arduino.h new file mode 100755 index 0000000000..1886eea031 --- /dev/null +++ b/variants/thinknode_m9/pins_arduino.h @@ -0,0 +1,64 @@ +// Need this file for ESP32-S3 +// No need to modify this file, changes to pins imported from variant.h +// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h + +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// Default SPI will be mapped to Radio +static const uint8_t SS = LORA_CS; +static const uint8_t SCK = SPI_SCK; +static const uint8_t MOSI = SPI_MOSI; +static const uint8_t MISO = SPI_MISO; + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SCL = I2C_SCL; +static const uint8_t SDA = I2C_SDA; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini new file mode 100755 index 0000000000..a7171726a8 --- /dev/null +++ b/variants/thinknode_m9/platformio.ini @@ -0,0 +1,160 @@ +[ThinkNode_M9] +extends = esp32_base +board = thinknode_m9 +board_check = true +board_build.partitions = default_16MB.csv +upload_protocol = esptool +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/thinknode_m9 + -I src/helpers/esp32 + -I src/helpers/sensors + -D THINKNODE_M9 + -D PIN_BUZZER=9 + -D PIN_BOARD_SCL=6 + -D PIN_BOARD_SDA=7 + -D P_LORA_NSS=39 + -D P_LORA_RESET=45 + -D P_LORA_BUSY=41 + -D P_LORA_SCLK=40 + -D P_LORA_MISO=38 + -D P_LORA_MOSI=47 + -D P_LORA_DIO_1=42 + -D USE_LR1110 + -D RF_SWITCH_TABLE + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 + -D LORA_TX_POWER=22 + -D DISPLAY_CLASS=ST7789Display + -D DISPLAY_FLIP_VERTICALLY=1 + -D DISPLAY_SCALE_X=2.5f ; 320 / 128 + -D DISPLAY_SCALE_Y=3.75f ; 240 / 64 + -D ST7789 + -D PIN_TFT_VDD_CTL=-1 + -D PIN_TFT_LEDA_CTL=17 + -D PIN_TFT_RST=14 + -D PIN_GPS_RX=3 + -D PIN_GPS_TX=2 + -D PIN_GPS_EN=11 + -D PIN_GPS_EN_ACTIVE=LOW + -D PIN_GPS_RESET=5 + -D PIN_GPS_RESET_ACTIVE=HIGH + -D GPS_BAUD_RATE=115200 + -D ENV_INCLUDE_GPS=1 + -D PIN_VBAT_READ=13 +build_src_filter = ${esp32_base.build_src_filter} + + + + + + + + + + + +<../variants/thinknode_m9> +lib_deps = ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit GFX Library @ ^1.12.1 + +[env:ThinkNode_M9_repeater_] +extends = ThinkNode_M9 +build_flags = + ${ThinkNode_M9.build_flags} + -D ADVERT_NAME='"ThinkNode M9 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_M9.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${ThinkNode_M9.lib_deps} + ${esp32_ota.lib_deps} + +[env:ThinkNode_M9_room_server_] +extends = ThinkNode_M9 +build_src_filter = ${ThinkNode_M9.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${ThinkNode_M9.build_flags} + -D ADVERT_NAME='"ThinkNode M9 Room Server"' + -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 +lib_deps = + ${ThinkNode_M9.lib_deps} + ${esp32_ota.lib_deps} + +[env:ThinkNode_M9_companion_radio_ble_] +extends = ThinkNode_M9 +build_flags = + ${ThinkNode_M9.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 +build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M9.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M9_companion_radio_usb_] +extends = ThinkNode_M9 +build_flags = + ${ThinkNode_M9.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M9.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M9_companion_radio_wifi_] +extends = ThinkNode_M9 +build_flags = + ${ThinkNode_M9.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 + ; -D MESH_PACKET_LOGGING=1 +build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M9.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M9_kiss_modem] +extends = ThinkNode_M9 +build_src_filter = ${ThinkNode_M9.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m9/target.cpp b/variants/thinknode_m9/target.cpp new file mode 100644 index 0000000000..3cbd7d758a --- /dev/null +++ b/variants/thinknode_m9/target.cpp @@ -0,0 +1,78 @@ +#include +#include "target.h" + +ThinkNodeM9Board 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); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef ENV_INCLUDE_GPS +#include +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#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 + +#ifndef LORA_CR + #define LORA_CR 5 +#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 + + 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_m9/target.h b/variants/thinknode_m9/target.h new file mode 100644 index 0000000000..f083a9f1b3 --- /dev/null +++ b/variants/thinknode_m9/target.h @@ -0,0 +1,29 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include +#endif + +extern ThinkNodeM9Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/thinknode_m9/variant.cpp b/variants/thinknode_m9/variant.cpp new file mode 100644 index 0000000000..4ac6a78276 --- /dev/null +++ b/variants/thinknode_m9/variant.cpp @@ -0,0 +1,7 @@ +#include "variant.h" +#include + + +void initVariant() { + +} diff --git a/variants/thinknode_m9/variant.h b/variants/thinknode_m9/variant.h new file mode 100755 index 0000000000..37f8b07630 --- /dev/null +++ b/variants/thinknode_m9/variant.h @@ -0,0 +1,114 @@ +/*Power*/ +#define VEXT_ENABLE 18 +#define VEXT_ON_VALUE LOW +#define PIN_GPS_EN 11 +#define GPS_EN_ACTIVE LOW + +/*Wire Interface*/ +#define WIRE_INTERFACES_COUNT 2 +// I2C keybuttons +#define I2C_SCL1 21 +#define I2C_SDA1 20 +#define KB_INT 12 +// I2C peripheral` +#define I2C_SCL 6 +#define I2C_SDA 7 + +/*LED*/ +#define PIN_LED 13 +#define KB_LED 46 + +/*BUZZER*/ +#define PIN_BUZZER 9 + +/*CHARGE_CHECK*/ +#define DONE 8 +#define EXT_PWR_DETECT 1 +#define EXT_CHRG_DETECT 1 +#define EXT_CHRG_DETECT_VALUE LOW +/*GPS*/ +#define GPS_L76K +#define GPS_BAUDRATE 9600 +#define PIN_GPS_RESET 5 +#define PIN_GPS_PPS 4 +#define PIN_GPS_STANDBY 10 // An output to wake GPS, low means allow sleep, high means force wake +#define GPS_TX_PIN 3 +#define GPS_RX_PIN 2 +#define GPS_THREAD_INTERVAL 50 + +/*SPI*/ +#define SPI_MOSI 47 +#define SPI_SCK 40 +#define SPI_MISO 38 + +/*SD Card*/ +#define SDCARD_CS 48 +#define SD_SPI_FREQUENCY 80000000U + +/*Screen*/ +// #define USE_ST7789 1 +#define ST7789_CS 16 +#define ST7789_RS 15 +#define ST7789_TE 19 +#define ST7789_SDA SPI_MOSI // MOSI +#define ST7789_SCK SPI_SCK +#define ST7789_RESET 14 +#define ST7789_MISO SPI_MISO +#define ST7789_BUSY -1 +#define ST7789_BL 17 +#define ST7789_SPI_HOST SPI2_HOST +#define SPI_FREQUENCY 80000000 +#define SPI_READ_FREQUENCY 16000000 + +#define USE_TFTDISPLAY 1 +#define TFT_CS ST7789_CS +#define TFT_BL ST7789_BL +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define TFT_PWM_FREQ 44000 +#define TFT_PWM_CHANNEL 7 +#define TFT_INVERT_LIGHT true +#define TFT_BACKLIGHT_ON LOW +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 +#define BRIGHTNESS_DEFAULT 128 + +#define LGFX_PANEL ST7789 +#define DISPLAY_SIZE 320x240 +#define LGFX_ROTATION 0 +#define LGFX_CFG_HOST SPI2_HOST +#define LGFX_PIN_SCK ST7789_SCK +#define LGFX_PIN_MOSI ST7789_SDA +#define LGFX_PIN_DC ST7789_RS +#define LGFX_PIN_CS ST7789_CS +#define LGFX_PIN_BL ST7789_BL +#define LGFX_SCREEN_WIDTH TFT_WIDTH +#define LGFX_SCREEN_HEIGHT TFT_HEIGHT +#define LGFX_INVERT_LIGHT true +#define LGFX_PWM_FREQ 44000 +#define LGFX_PWM_CHANNEL 7 + +/*Lora radio*/ +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 39 +#define LORA_RESET 45 +#define LORA_DIO0 41 + +#define USE_LR1110 +#define LR1110_IRQ_PIN 42 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO0 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.3 +#define LR11X0_DIO_AS_RF_SWITCH + +/*RTC*/ +#define PCF8563_RTC 0x51 From b09b8a057dca5c6d4fc3ad0d1b0dbc3028b43a51 Mon Sep 17 00:00:00 2001 From: mathison Date: Tue, 14 Jul 2026 00:22:34 -0700 Subject: [PATCH 73/74] Add RAK12501 L76K PCAS configuration --- variants/rak3401/target.cpp | 93 ++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index 5309e6b22b..4b13b7bc19 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -1,4 +1,5 @@ #include +#include #include "target.h" #include @@ -26,7 +27,96 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + + // RAK3401 1W Booster GPS builds use the RAK12501 / Quectel L76K by default. + // This config is deliberately local to the RAK3401 target so the shared GPS + // manager does not learn another board-specific L76K init path. + #ifndef RAK12501_L76K_GPS + #define RAK12501_L76K_GPS 1 + #endif + + #if defined(RAK12501_L76K_GPS) + #ifndef RAK12501_L76K_NAV_MODE + #define RAK12501_L76K_NAV_MODE 3 + #endif + + #ifndef RAK12501_L76K_BOOT_DELAY_MS + #define RAK12501_L76K_BOOT_DELAY_MS 1000 + #endif + + static void rak12501SendNMEACommand(Stream& serial, const char* body) { + uint8_t checksum = 0; + for (const char* p = body; *p; ++p) { + checksum ^= (uint8_t)*p; + } + + serial.print('$'); + serial.print(body); + serial.print('*'); + if (checksum < 0x10) { + serial.print('0'); + } + serial.print(checksum, HEX); + serial.print("\r\n"); + } + + static void rak12501InjectKnownTime() { + uint32_t now = rtc_clock.getCurrentTime(); + + // Only inject if the board clock is plausible. Avoid feeding the GPS the + // zero/default RTC value on first boot. 2020-01-01 is a conservative floor. + if (now < 1577836800UL) { + return; + } + + DateTime dt(now); + char body[40]; + snprintf(body, sizeof(body), "PMTK740,%04u,%02u,%02u,%02u,%02u,%02u", + dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()); + rak12501SendNMEACommand(Serial1, body); + delay(250); + } + + static void rak12501ConfigureL76K() { + MESH_DEBUG_PRINTLN("Configuring RAK12501/L76K GPS"); + + // Best-effort MTK aiding: if RTC has a known time, provide UTC time before + // the main config. Location/almanac injection is intentionally not included; + // it needs a fresh/accurate location source or downloadable EPO/almanac data. + rak12501InjectKnownTime(); + + // Meshtastic-style L76K init: + // GPS + GLONASS + BeiDou + // NMEA RMC + GGA only to reduce serial traffic for the companion app + // Vehicle mode by default ($PCAS11,3) + rak12501SendNMEACommand(Serial1, "PCAS04,7"); + delay(250); + rak12501SendNMEACommand(Serial1, "PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0"); + delay(250); + + char nav_mode[16]; + uint8_t mode = (RAK12501_L76K_NAV_MODE <= 7) ? RAK12501_L76K_NAV_MODE : 3; + snprintf(nav_mode, sizeof(nav_mode), "PCAS11,%u", mode); + rak12501SendNMEACommand(Serial1, nav_mode); + delay(250); + } + + class RAK12501L76KLocationProvider : public MicroNMEALocationProvider { + public: + using MicroNMEALocationProvider::MicroNMEALocationProvider; + + void reset() override { + MicroNMEALocationProvider::reset(); + delay(RAK12501_L76K_BOOT_DELAY_MS); + rak12501ConfigureL76K(); + } + }; + + RAK12501L76KLocationProvider nmea = RAK12501L76KLocationProvider(Serial1, &rtc_clock); + #else + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + #endif + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; @@ -41,4 +131,3 @@ mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity } - From ecbd32775b1bc7cc5a03f88756fd8ade8c4f6917 Mon Sep 17 00:00:00 2001 From: mathison Date: Tue, 14 Jul 2026 00:23:53 -0700 Subject: [PATCH 74/74] Fix RAK3401 RAK12501 UART pin mapping --- variants/rak3401/variant.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 9882788608..f031cb3444 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -180,20 +180,20 @@ static const uint8_t AREF = PIN_AREF; #define PIN_3V3_EN (34) #define WB_IO2 PIN_3V3_EN -// RAK1910 GPS module -// If using the wisblock GPS module and pluged into Port A on WisBlock base -// IO1 is hooked to PPS (pin 12 on header) = gpio 17 -// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). -// Therefore must be 1 to keep peripherals powered -// Power is on the controllable 3V3_S rail +// RAK12501 UART GNSS module +// RAK12501 uses the Quectel L76K and speaks NMEA over UART. +// The RAK3401 1W Booster keeps WB_IO2 high for the shared peripheral/PA rail, +// so GPS power saving must not toggle PIN_3V3_EN as a GPS-only enable pin. +// +// EnvironmentSensorManager currently calls Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX). +// Keep these definitions aligned with the existing MeshCore RAK4631 convention so +// that call expands to setPins(MCU_RX, MCU_TX). #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 -#define GPS_ADDRESS 0x42 //i2c address for GPS +#define GPS_ADDRESS 0x42 // kept for compatibility; RAK12501/L76K uses UART NMEA here // Battery // The battery sense is hooked to pin A0 (5)