From c89a0e9929f4247d40569f568ccb1d216cc66762 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 22 Jan 2026 15:03:40 +0800 Subject: [PATCH 001/191] 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 82c8c21d9..2035d7897 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 8c745613e..f371cb465 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 1a3b4d6e0..e468b84e8 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 da1bac5b3..693ebfeea 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 a5fcc1484..9712a2072 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 000000000..65f178b0c --- /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 7bfbac85a..3eb51f0a5 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 ad79f7171..060f20c38 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 e301a2735..c535f68ec 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 @@ public: 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 14956619f..3c1b378df 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 002/191] 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 6cc2ac77a..1760abeaa 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 65f178b0c..4df901b7f 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 c9172aa1a..7eb44bfaf 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 003/191] 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 3c69625a1..88ef39245 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 1760abeaa..f0271c889 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 bb6a860b8..d20ebc6d1 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 a21822363..a21741db5 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 e9ff9387e..346f4dfc9 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 4df901b7f..9ef8abaa6 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 3eb51f0a5..1bc7e7fa7 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 7eb44bfaf..fa2342026 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 3fc786726..124e3afb3 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 @@ public: 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 70b51bd096da36d5186e08c46819d7e49e69fe9b Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 9 Mar 2026 14:52:01 -0700 Subject: [PATCH 004/191] Add native Ethernet support for RAK4631 repeater, room server, and companion Add W5100S Ethernet adapter support for RAK4631-based firmware, enabling TCP CLI access on port 23 as an alternative to BLE/Serial connections. - New SerialEthernetInterface for nRF52 with DHCP, reconnection handling, and shared WB_IO2 power pin management with GPS module - Ethernet build targets for repeater, room server, and companion firmware - Prevent GPS from toggling WB_IO2 when Ethernet module is active - CI build check for all three ETH firmware targets Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr-build-check.yml | 3 + examples/companion_radio/main.cpp | 56 +++- examples/simple_repeater/main.cpp | 167 ++++++++++ examples/simple_room_server/main.cpp | 157 +++++++++ src/helpers/nrf52/SerialEthernetInterface.cpp | 303 ++++++++++++++++++ src/helpers/nrf52/SerialEthernetInterface.h | 83 +++++ .../sensors/EnvironmentSensorManager.cpp | 7 + variants/rak4631/RAK4631Board.cpp | 4 + variants/rak4631/platformio.ini | 73 ++++- 9 files changed, 842 insertions(+), 11 deletions(-) create mode 100644 src/helpers/nrf52/SerialEthernetInterface.cpp create mode 100644 src/helpers/nrf52/SerialEthernetInterface.h diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 5ba677cd3..2d9dbf79f 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -23,8 +23,11 @@ jobs: - Heltec_v3_room_server # nRF52 - RAK_4631_companion_radio_ble + - RAK_4631_companion_radio_eth - RAK_4631_repeater + - RAK_4631_repeater_eth - RAK_4631_room_server + - RAK_4631_room_server_eth # RP2040 - PicoW_repeater # STM32 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca4..a3c83f329 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,19 +12,21 @@ static uint32_t _atoi(const char* sp) { return n; } +uint32_t tick_count = 0; + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) #include DataStore store(InternalFS, QSPIFlash, rtc_clock); #else - #if defined(EXTRAFS) - #include - CustomLFS ExtraFS(0xD4000, 0x19000, 128); - DataStore store(InternalFS, ExtraFS, rtc_clock); - #else - DataStore store(InternalFS, rtc_clock); - #endif + #if defined(EXTRAFS) + #include + CustomLFS ExtraFS(0xD4000, 0x19000, 128); + DataStore store(InternalFS, ExtraFS, rtc_clock); + #else + DataStore store(InternalFS, rtc_clock); + #endif #endif #elif defined(RP2040_PLATFORM) #include @@ -74,13 +76,21 @@ static uint32_t _atoi(const char* sp) { #ifdef BLE_PIN_CODE #include SerialBLEInterface serial_interface; + #elif defined(ETH_ENABLED) + #include + SerialEthernetInterface serial_interface; #else #include ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; + #ifdef ETH_ENABLED + #include + SerialEthernetInterface serial_interface; + #elif + #include + ArduinoSerialInterface serial_interface; + #endif #else #error "need to define a serial interface" #endif @@ -107,7 +117,6 @@ void halt() { void setup() { Serial.begin(115200); - board.begin(); #ifdef DISPLAY_CLASS @@ -152,6 +161,23 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); +#elif ETH_ENABLED + + Serial.print("Waiting for serial to connect...\n"); + time_t timeout = millis(); + // Initialize Serial for debug output. + while (!Serial) + { + if ((millis() - timeout) < 5000) { delay(100); } else { break; } + } + Serial.print("Initalizing ethernet adapter....\n"); + bool result = serial_interface.begin(); + if (!result) { + while (true) + { + delay(1); // Do nothing, just love you. + } + } #else serial_interface.begin(Serial); #endif @@ -225,4 +251,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + // Debugging only... making sure something is alive. + tick_count++; + if (tick_count % 5000 == 0) { + Serial.print("."); + } + +#ifdef ETH_ENABLED + serial_interface.maintain(); +#endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d226d1fa7..88157171a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,6 +8,123 @@ static UITask ui_task(display); #endif +#ifdef ETH_ENABLED + #include + #include + + #define PIN_SPI1_MISO (29) + #define PIN_SPI1_MOSI (30) + #define PIN_SPI1_SCK (3) + SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + + #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_RESET 21 + #define PIN_ETHERNET_SS 26 + + #ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 23 // telnet port for CLI access + #endif + + #define ETH_RETRY_INTERVAL_MS 30000 + + static EthernetServer eth_server(ETH_TCP_PORT); + static EthernetClient eth_client; + static volatile bool eth_running = false; + + static void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; + } + + // FreeRTOS task: handles hw init, DHCP, and retries in the background + static void eth_task(void* param) { + (void)param; + + // Hardware init + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateDeviceMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + // Retry loop: keep trying until we get an IP + while (!eth_running) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + + if (Ethernet.linkStatus() == LinkOFF) { + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + Serial.println("ETH: Link detected, attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + Serial.println("ETH: DHCP failed, will retry"); + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + IPAddress ip = Ethernet.localIP(); + Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + Serial.printf("ETH: Listening on TCP port %d\n", ETH_TCP_PORT); + eth_server.begin(); + eth_running = true; + } + + // DHCP succeeded, task is done + vTaskDelete(NULL); + } + + static void eth_start_task() { + xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + } + + // Format ethernet status into reply buffer. Returns true if command was handled. + static bool eth_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth") != 0) return false; + if (!eth_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + } + return true; + } + + // Check for new TCP client connections + static void eth_check_client() { + if (eth_client && eth_client.connected()) return; + + auto newClient = eth_server.available(); + if (newClient) { + if (eth_client) eth_client.stop(); + eth_client = newClient; + IPAddress ip = eth_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + eth_client.println("MeshCore Repeater CLI"); + eth_client.print("> "); + } + } +#endif + StdRNG fast_rng; SimpleMeshTables tables; @@ -18,6 +135,9 @@ void halt() { } static char command[160]; +#ifdef ETH_ENABLED +static char eth_command[160]; +#endif // For power saving unsigned long lastActive = 0; // mark last active time @@ -85,6 +205,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETH_ENABLED + eth_command[0] = 0; +#endif sensors.begin(); @@ -94,6 +217,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETH_ENABLED + eth_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -101,6 +228,7 @@ void setup() { } void loop() { + // Handle Serial CLI int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); @@ -119,6 +247,10 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; +#ifdef ETH_ENABLED + if (!eth_handle_command(command, reply)) +#endif the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -127,6 +259,41 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETH_ENABLED + if (eth_running) { + eth_check_client(); + Ethernet.maintain(); + } + + if (eth_running && eth_client && eth_client.connected()) { + int elen = strlen(eth_command); + while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { + char c = eth_client.read(); + if (c == '\n') continue; // ignore LF + eth_command[elen++] = c; + eth_command[elen] = 0; + if (c == '\r') break; + } + if (elen == sizeof(eth_command)-1) { + eth_command[sizeof(eth_command)-1] = '\r'; + } + + if (elen > 0 && eth_command[elen - 1] == '\r') { + eth_command[elen - 1] = 0; + eth_client.println(); + char reply[160]; + reply[0] = 0; + if (!eth_handle_command(eth_command, reply)) + the_mesh.handleCommand(0, eth_command, reply); + if (reply[0]) { + eth_client.print(" -> "); eth_client.println(reply); + } + eth_client.print("> "); + eth_command[0] = 0; + } + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d..a84a7ee93 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,114 @@ #include "MyMesh.h" +#ifdef ETH_ENABLED + #include + #include + + #define PIN_SPI1_MISO (29) + #define PIN_SPI1_MOSI (30) + #define PIN_SPI1_SCK (3) + SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + + #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_RESET 21 + #define PIN_ETHERNET_SS 26 + + #ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 23 + #endif + + #define ETH_RETRY_INTERVAL_MS 30000 + + static EthernetServer eth_server(ETH_TCP_PORT); + static EthernetClient eth_client; + static volatile bool eth_running = false; + + static void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; + } + + static void eth_task(void* param) { + (void)param; + + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateDeviceMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + while (!eth_running) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + if (Ethernet.linkStatus() == LinkOFF) { + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + Serial.println("ETH: Link detected, attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + Serial.println("ETH: DHCP failed, will retry"); + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + IPAddress ip = Ethernet.localIP(); + Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + Serial.printf("ETH: Listening on TCP port %d\n", ETH_TCP_PORT); + eth_server.begin(); + eth_running = true; + } + vTaskDelete(NULL); + } + + static void eth_start_task() { + xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + } + + static bool eth_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth") != 0) return false; + if (!eth_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + } + return true; + } + + static void eth_check_client() { + if (eth_client && eth_client.connected()) return; + auto newClient = eth_server.available(); + if (newClient) { + if (eth_client) eth_client.stop(); + eth_client = newClient; + IPAddress ip = eth_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + eth_client.println("MeshCore Room Server CLI"); + eth_client.print("> "); + } + } +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -17,6 +125,9 @@ void halt() { } static char command[MAX_POST_TEXT_LEN+1]; +#ifdef ETH_ENABLED +static char eth_command[MAX_POST_TEXT_LEN+1]; +#endif void setup() { Serial.begin(115200); @@ -67,6 +178,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETH_ENABLED + eth_command[0] = 0; +#endif sensors.begin(); @@ -76,6 +190,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETH_ENABLED + eth_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -99,6 +217,10 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; +#ifdef ETH_ENABLED + if (!eth_handle_command(command, reply)) +#endif the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -107,6 +229,41 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETH_ENABLED + if (eth_running) { + eth_check_client(); + Ethernet.maintain(); + } + + if (eth_running && eth_client && eth_client.connected()) { + int elen = strlen(eth_command); + while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { + char c = eth_client.read(); + if (c == '\n') continue; + eth_command[elen++] = c; + eth_command[elen] = 0; + if (c == '\r') break; + } + if (elen == sizeof(eth_command)-1) { + eth_command[sizeof(eth_command)-1] = '\r'; + } + + if (elen > 0 && eth_command[elen - 1] == '\r') { + eth_command[elen - 1] = 0; + eth_client.println(); + char reply[160]; + reply[0] = 0; + if (!eth_handle_command(eth_command, reply)) + the_mesh.handleCommand(0, eth_command, reply); + if (reply[0]) { + eth_client.print(" -> "); eth_client.println(reply); + } + eth_client.print("> "); + eth_command[0] = 0; + } + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp new file mode 100644 index 000000000..ba9515312 --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -0,0 +1,303 @@ +#include "SerialEthernetInterface.h" +#include +#include + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETH_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 +//#define STATIC_IP 1 + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool SerialEthernetInterface::begin() { + + ETH_DEBUG_PRINTLN("Ethernet initalizing"); + +#ifdef PIN_ETH_POWER_EN + ETH_DEBUG_PRINTLN("Ethernet power enable"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + delay(100); + ETH_DEBUG_PRINTLN("Ethernet power enabled"); +#endif + +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. + ETH_DEBUG_PRINTLN("Ethernet reset pulse"); +#endif + + uint8_t mac[6]; + generateDeviceMac(mac); + ETH_DEBUG_PRINTLN( + "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + ETH_DEBUG_PRINTLN("Init"); + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + // Hardcode IP address for now + #ifdef STATIC_IP + IPAddress ip(192, 168, 8, 118); + IPAddress gateway(192, 168, 8, 1); + IPAddress subnet(255, 255, 255, 0); + IPAddress dns(192, 168, 8, 1); + Ethernet.begin(mac, ip, dns, gateway, subnet); + #else + ETH_DEBUG_PRINTLN("Begin"); + if (Ethernet.begin(mac) == 0) { + ETH_DEBUG_PRINTLN("Begin failed."); + + // DHCP failed -- let's figure out why + if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. + { + ETH_DEBUG_PRINTLN("Ethernet hardware not found."); + return false; + } + if (Ethernet.linkStatus() == LinkOFF) // No physical connection + { + ETH_DEBUG_PRINTLN("Ethernet cable not connected."); + return false; + } + ETH_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + return false; + } + #endif + ETH_DEBUG_PRINTLN("Ethernet begin complete"); + IPAddress ip = Ethernet.localIP(); + ETH_DEBUG_PRINT_IP("IP", ip); + + IPAddress subnet = Ethernet.subnetMask(); + ETH_DEBUG_PRINT_IP("Subnet", subnet); + + IPAddress gateway = Ethernet.gatewayIP(); + ETH_DEBUG_PRINT_IP("Gateway", gateway); + + server.begin(); // start listening for clients + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", TCP_PORT); + + return true; +} + +void SerialEthernetInterface::enable() { + if (_isEnabled) return; + + _isEnabled = true; + clearBuffers(); +} + +void SerialEthernetInterface::disable() { + _isEnabled = false; +} + +size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETH_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETH_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool SerialEthernetInterface::isWriteBusy() const { + return false; +} + +size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { + // check if new client connected + if (client && client.connected()) { + // Avoid polling for new clients while an active connection exists. + } else { + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETH_DEBUG_PRINTLN( + "New client available %u.%u.%u.%u:%u", + new_ip[0], + new_ip[1], + new_ip[2], + new_ip[3], + new_port); + if (client && client.connected()) { + IPAddress cur_ip = client.remoteIP(); + uint16_t cur_port = client.remotePort(); + ETH_DEBUG_PRINTLN( + "Current client %u.%u.%u.%u:%u", + cur_ip[0], + cur_ip[1], + cur_ip[2], + cur_ip[3], + cur_port); + if (cur_ip == new_ip && cur_port == new_port) { + ETH_DEBUG_PRINTLN("Ignoring duplicate client"); + return 0; + } + } + + deviceConnected = false; + if (client) { + ETH_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETH_DEBUG_PRINTLN("Switched to new client"); + } + } + + if (client.connected()) { + if (!deviceConnected) { + ETH_DEBUG_PRINTLN( + "Got connection %u.%u.%u.%u:%u", + client.remoteIP()[0], + client.remoteIP()[1], + client.remoteIP()[2], + client.remoteIP()[3], + client.remotePort()); + deviceConnected = true; + } + } else { + if (deviceConnected) { + deviceConnected = false; + ETH_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETH_RAW_LINE + ETH_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETH_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETH_DEBUG_LOGGING && ARDUINO + ETH_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + client.write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (client.available()) { + int c = client.read(); + if (c < 0) break; + +#if ETH_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) { + out_len = MAX_FRAME_SIZE; + } + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETH_DEBUG_LOGGING && ARDUINO + ETH_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} + +bool SerialEthernetInterface::isConnected() const { + return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; +} + +void SerialEthernetInterface::generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + + mac[0] = 0x02; + mac[1] = 0x92; + mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; +} + +void SerialEthernetInterface::maintain() { + Ethernet.maintain(); +} diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h new file mode 100644 index 000000000..39eefbb4f --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -0,0 +1,83 @@ + +#include "helpers/BaseSerialInterface.h" +#include +#include + +// expects ETH_ENABLED = 1 +#define TCP_PORT 5000 +// define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class SerialEthernetInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + unsigned long adv_restart_time; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + EthernetServer server; + EthernetClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + recv_queue_len = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + SerialEthernetInterface() : server(EthernetServer(TCP_PORT)) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = recv_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + + void maintain(); + +private: + void generateDeviceMac(uint8_t mac[6]); +}; + + +#if ETH_DEBUG_LOGGING && ARDUINO + #include + #define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETH_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETH_DEBUG_PRINT(...) {} + #define ETH_DEBUG_PRINTLN(...) {} + #define ETH_DEBUG_PRINT_IP(...) {} +#endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 07807011d..ee09d31d5 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -628,6 +628,13 @@ void EnvironmentSensorManager::rakGPSInit(){ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ + #if defined(ETH_ENABLED) && defined(RAK_BOARD) + if (ioPin == WB_IO2) { + // WB_IO2 powers the Ethernet module on RAK baseboards. + return false; + } + #endif + //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 9fb47b432..767c833f4 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -36,6 +36,10 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); #endif +#ifdef RAK_ETH_ENABLE + beginETH(); +#endif + #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 737ef5652..39765b01e 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -46,6 +46,26 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +[env:RAK_4631_repeater_eth] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK4631 Repeater ETH"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D ETH_ENABLED=1 + -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${rak4631.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + [env:RAK_4631_repeater_bridge_rs232_serial1] extends = rak4631 build_flags = @@ -108,6 +128,26 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> +[env:RAK_4631_room_server_eth] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Test Room ETH"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D ETH_ENABLED=1 + -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${rak4631.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + [env:RAK_4631_companion_radio_usb] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -129,6 +169,37 @@ lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:RAK_4631_companion_radio_eth] +extends = rak4631 +board_build.ldscript = boards/nrf52840_s140_v6.ld +board_upload.maximum_size = 712704 +build_unflags = + -D EXTRAFS=1 +build_flags = + ${rak4631.build_flags} + -I examples/companion_radio/ui-new + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D ETH_ENABLED=1 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 + -D MESH_DEBUG=1 + -D ETH_DEBUG_LOGGING=1 +build_src_filter = ${rak4631.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${rak4631.lib_deps} + densaugeo/base64 @ ~1.4.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + + [env:RAK_4631_companion_radio_ble] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -192,4 +263,4 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} +<../examples/kiss_modem/> lib_deps = - ${rak4631.lib_deps} \ No newline at end of file + ${rak4631.lib_deps} From aea64e1f1907650e993fbb82f06a90feb076d546 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 9 Mar 2026 15:15:21 -0700 Subject: [PATCH 005/191] Address PR review feedback for Ethernet support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add #pragma once to SerialEthernetInterface.h - Rename TCP_PORT to ETH_TCP_PORT with #ifndef guard - Fix typos: "initalizing" → "initializing" - Fix #elif without condition → #else for STM32 block - Replace infinite loop on ETH init failure with halt() - Remove heartbeat Serial.print(".") output - Remove dead beginETH() call (ETH_ENABLED, not RAK_ETH_ENABLE) - Comment out MESH_DEBUG and ETH_DEBUG_LOGGING build flags Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/main.cpp | 19 +++++-------------- src/helpers/nrf52/SerialEthernetInterface.cpp | 4 ++-- src/helpers/nrf52/SerialEthernetInterface.h | 8 +++++--- variants/rak4631/RAK4631Board.cpp | 3 --- variants/rak4631/platformio.ini | 8 ++++---- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a3c83f329..6dae1356d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,7 +12,6 @@ static uint32_t _atoi(const char* sp) { return n; } -uint32_t tick_count = 0; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include @@ -87,7 +86,7 @@ uint32_t tick_count = 0; #ifdef ETH_ENABLED #include SerialEthernetInterface serial_interface; - #elif + #else #include ArduinoSerialInterface serial_interface; #endif @@ -161,7 +160,7 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif ETH_ENABLED +#elif defined(ETH_ENABLED) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); @@ -170,13 +169,11 @@ void setup() { { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } - Serial.print("Initalizing ethernet adapter....\n"); + Serial.print("Initializing ethernet adapter....\n"); bool result = serial_interface.begin(); if (!result) { - while (true) - { - delay(1); // Do nothing, just love you. - } + Serial.println("ETH: Init failed, halting"); + halt(); } #else serial_interface.begin(Serial); @@ -252,12 +249,6 @@ void loop() { #endif rtc_clock.tick(); - // Debugging only... making sure something is alive. - tick_count++; - if (tick_count % 5000 == 0) { - Serial.print("."); - } - #ifdef ETH_ENABLED serial_interface.maintain(); #endif diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index ba9515312..3dc206719 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -20,7 +20,7 @@ SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); bool SerialEthernetInterface::begin() { - ETH_DEBUG_PRINTLN("Ethernet initalizing"); + ETH_DEBUG_PRINTLN("Ethernet initializing"); #ifdef PIN_ETH_POWER_EN ETH_DEBUG_PRINTLN("Ethernet power enable"); @@ -90,7 +90,7 @@ bool SerialEthernetInterface::begin() { ETH_DEBUG_PRINT_IP("Gateway", gateway); server.begin(); // start listening for clients - ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", TCP_PORT); + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETH_TCP_PORT); return true; } diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 39eefbb4f..7adf3569e 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -1,10 +1,12 @@ +#pragma once #include "helpers/BaseSerialInterface.h" #include #include -// expects ETH_ENABLED = 1 -#define TCP_PORT 5000 +#ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 5000 +#endif // define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets class SerialEthernetInterface : public BaseSerialInterface { @@ -42,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { protected: public: - SerialEthernetInterface() : server(EthernetServer(TCP_PORT)) { + SerialEthernetInterface() : server(EthernetServer(ETH_TCP_PORT)) { deviceConnected = false; _isEnabled = false; _last_write = 0; diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 767c833f4..08286604c 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -36,9 +36,6 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); #endif -#ifdef RAK_ETH_ENABLE - beginETH(); -#endif #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 39765b01e..96c8c73d2 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -57,7 +57,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D ETH_ENABLED=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> @@ -139,7 +139,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' -D ETH_ENABLED=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> @@ -187,8 +187,8 @@ build_flags = -D ETH_ENABLED=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 - -D MESH_DEBUG=1 - -D ETH_DEBUG_LOGGING=1 +; -D MESH_DEBUG=1 +; -D ETH_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From ffe0853d7fee02ab1ca4768d421bd081188363b1 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Tue, 10 Mar 2026 20:58:34 -0700 Subject: [PATCH 006/191] Add Ethernet documentation for RAK4631 ETH support Co-Authored-By: Claude Opus 4.6 --- docs/cli_commands.md | 23 +++++++++++++++++++++++ docs/faq.md | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1d3430db2..ea2c941c2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -19,6 +19,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - [GPS](#gps-when-gps-support-is-compiled-in) - [Sensors](#sensors-when-sensor-support-is-compiled-in) - [Bridge](#bridge-when-bridge-support-is-compiled-in) + - [Ethernet](#ethernet-when-ethernet-support-is-compiled-in) --- @@ -881,3 +882,25 @@ region save **Default:** Varies by board --- + +### Ethernet (when Ethernet support is compiled in) + +Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_eth` firmware variants (e.g. `RAK_4631_repeater_eth`) to enable this feature. + +--- + +#### View Ethernet connection status +**Usage:** +- `eth` + +**Output:** +- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:5000`) +- `ETH: not connected` when Ethernet is not active + +**Notes:** +- The Ethernet interface obtains an IP address via DHCP automatically on boot. +- A TCP server listens on port 5000 (default) for CLI connections. +- For repeaters and room servers, connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. +- For companion radio firmware, the Ethernet interface replaces BLE/USB as the transport to companion apps. + +--- diff --git a/docs/faq.md b/docs/faq.md index 220b8971c..84ab02261 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -97,6 +97,7 @@ A list of frequently-asked questions and answers for MeshCore - [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code) - [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3) - [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-a-ebyte-e22-900m30s-or-a-e22-900m33s-module-what-should-their-transmit-power-be-set-to) + - [7.8. Q: How do I use Ethernet with a RAK4631?](#78-q-how-do-i-use-ethernet-with-a-rak4631) ## 1. Introduction @@ -878,3 +879,27 @@ For companion radios, you can set these radios' transmit power in the smartphone | **Heltec V4** | Standard Output | 10 dBm | 22 dBm | | | | High Output | 22 dBm | 28 dBm | | --- + +### 7.8. Q: How do I use Ethernet with a RAK4631? + **A:** +MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.rakwireless.com/product-categories/wisblock/rak13800/datasheet/) WisBlock Ethernet module (based on the W5100S chip). + +**Hardware required:** +- RAK4631 WisBlock Core +- RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) +- RAK13800 WisBlock Ethernet module +- Ethernet cable connected to a network with a DHCP server + +**Firmware:** +Flash one of the Ethernet-enabled firmware variants: +- `RAK_4631_repeater_eth` - Repeater with Ethernet CLI access +- `RAK_4631_room_server_eth` - Room server with Ethernet CLI access +- `RAK_4631_companion_radio_eth` - Companion radio over Ethernet (replaces BLE) + +**Connecting:** +- The device obtains an IP address via DHCP automatically on boot. +- For repeaters and room servers, connect to the device on TCP port 5000 using any TCP client (e.g. `nc 5000` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. +- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. +- Use the `eth` CLI command to check connection status and see the assigned IP address. + +--- From 3e9ceba24a65a6f13409b48614909237e737f5aa Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Tue, 10 Mar 2026 22:35:31 -0700 Subject: [PATCH 007/191] Updates based on PR review feedback from liamcottle --- .github/workflows/pr-build-check.yml | 6 +- examples/companion_radio/main.cpp | 20 +-- examples/simple_repeater/main.cpp | 141 +++++++++--------- examples/simple_room_server/main.cpp | 141 +++++++++--------- src/helpers/nrf52/EthernetMac.h | 13 ++ src/helpers/nrf52/SerialEthernetInterface.cpp | 117 +++++++-------- src/helpers/nrf52/SerialEthernetInterface.h | 27 ++-- .../sensors/EnvironmentSensorManager.cpp | 2 +- variants/rak4631/platformio.ini | 19 ++- 9 files changed, 237 insertions(+), 249 deletions(-) create mode 100644 src/helpers/nrf52/EthernetMac.h diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 2d9dbf79f..9292338fb 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -23,11 +23,11 @@ jobs: - Heltec_v3_room_server # nRF52 - RAK_4631_companion_radio_ble - - RAK_4631_companion_radio_eth + - RAK_4631_companion_radio_ethernet - RAK_4631_repeater - - RAK_4631_repeater_eth + - RAK_4631_repeater_ethernet - RAK_4631_room_server - - RAK_4631_room_server_eth + - RAK_4631_room_server_ethernet # RP2040 - PicoW_repeater # STM32 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dae1356d..9fa1e3810 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -75,7 +75,7 @@ static uint32_t _atoi(const char* sp) { #ifdef BLE_PIN_CODE #include SerialBLEInterface serial_interface; - #elif defined(ETH_ENABLED) + #elif defined(ETHERNET_ENABLED) #include SerialEthernetInterface serial_interface; #else @@ -83,7 +83,7 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #ifdef ETH_ENABLED + #ifdef ETHERNET_ENABLED #include SerialEthernetInterface serial_interface; #else @@ -160,18 +160,14 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(ETH_ENABLED) - +#elif defined(ETHERNET_ENABLED) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); - // Initialize Serial for debug output. - while (!Serial) - { + while (!Serial) { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } - Serial.print("Initializing ethernet adapter....\n"); - bool result = serial_interface.begin(); - if (!result) { + Serial.println("Initializing Ethernet adapter..."); + if (!serial_interface.begin()) { Serial.println("ETH: Init failed, halting"); halt(); } @@ -249,7 +245,7 @@ void loop() { #endif rtc_clock.tick(); -#ifdef ETH_ENABLED - serial_interface.maintain(); +#ifdef ETHERNET_ENABLED + serial_interface.loop(); #endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 88157171a..0f37aa6b7 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,45 +8,38 @@ static UITask ui_task(display); #endif -#ifdef ETH_ENABLED +#ifdef ETHERNET_ENABLED #include #include + #include #define PIN_SPI1_MISO (29) #define PIN_SPI1_MOSI (30) #define PIN_SPI1_SCK (3) - SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_POWER_EN WB_IO2 #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 - #ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 23 // telnet port for CLI access + #ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 // telnet port for CLI access #endif - #define ETH_RETRY_INTERVAL_MS 30000 + #define ETHERNET_RETRY_INTERVAL_MS 30000 - static EthernetServer eth_server(ETH_TCP_PORT); - static EthernetClient eth_client; - static volatile bool eth_running = false; - - static void generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; - } + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); + static EthernetClient ethernet_client; + static volatile bool ethernet_running = false; // FreeRTOS task: handles hw init, DHCP, and retries in the background - static void eth_task(void* param) { + static void ethernet_task(void* param) { (void)param; // Hardware init Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); vTaskDelay(pdMS_TO_TICKS(100)); pinMode(PIN_ETHERNET_RESET, OUTPUT); @@ -54,8 +47,8 @@ vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; generateDeviceMac(mac); @@ -63,7 +56,7 @@ mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // Retry loop: keep trying until we get an IP - while (!eth_running) { + while (!ethernet_running) { if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("ETH: Hardware not found, giving up"); vTaskDelete(NULL); @@ -71,56 +64,56 @@ } if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } Serial.println("ETH: Link detected, attempting DHCP..."); if (Ethernet.begin(mac, 10000, 2000) == 0) { Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } IPAddress ip = Ethernet.localIP(); Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - Serial.printf("ETH: Listening on TCP port %d\n", ETH_TCP_PORT); - eth_server.begin(); - eth_running = true; + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; } // DHCP succeeded, task is done vTaskDelete(NULL); } - static void eth_start_task() { - xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); } // Format ethernet status into reply buffer. Returns true if command was handled. - static bool eth_handle_command(const char* command, char* reply) { + static bool ethernet_handle_command(const char* command, char* reply) { if (strcmp(command, "eth") != 0) return false; - if (!eth_running) { + if (!ethernet_running) { strcpy(reply, "ETH: not connected"); } else { IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); } return true; } // Check for new TCP client connections - static void eth_check_client() { - if (eth_client && eth_client.connected()) return; + static void ethernet_check_client() { + if (ethernet_client && ethernet_client.connected()) return; - auto newClient = eth_server.available(); + auto newClient = ethernet_server.available(); if (newClient) { - if (eth_client) eth_client.stop(); - eth_client = newClient; - IPAddress ip = eth_client.remoteIP(); + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - eth_client.println("MeshCore Repeater CLI"); - eth_client.print("> "); + ethernet_client.println("MeshCore Repeater CLI"); + ethernet_client.print("> "); } } #endif @@ -135,8 +128,8 @@ void halt() { } static char command[160]; -#ifdef ETH_ENABLED -static char eth_command[160]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[160]; #endif // For power saving @@ -205,8 +198,8 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; -#ifdef ETH_ENABLED - eth_command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; #endif sensors.begin(); @@ -217,8 +210,8 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif -#ifdef ETH_ENABLED - eth_start_task(); +#ifdef ETHERNET_ENABLED + ethernet_start_task(); #endif // send out initial zero hop Advertisement to the mesh @@ -248,10 +241,13 @@ void loop() { command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; reply[0] = 0; -#ifdef ETH_ENABLED - if (!eth_handle_command(command, reply)) -#endif +#ifdef ETHERNET_ENABLED + if (!ethernet_handle_command(command, reply)) { + the_mesh.handleCommand(0, command, reply); + } +#else the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! +#endif if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -259,37 +255,38 @@ void loop() { command[0] = 0; // reset command buffer } -#ifdef ETH_ENABLED - if (eth_running) { - eth_check_client(); +#ifdef ETHERNET_ENABLED + if (ethernet_running) { + ethernet_check_client(); Ethernet.maintain(); } - if (eth_running && eth_client && eth_client.connected()) { - int elen = strlen(eth_command); - while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { - char c = eth_client.read(); - if (c == '\n') continue; // ignore LF - eth_command[elen++] = c; - eth_command[elen] = 0; - if (c == '\r') break; + if (ethernet_running && ethernet_client && ethernet_client.connected()) { + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; } - if (elen == sizeof(eth_command)-1) { - eth_command[sizeof(eth_command)-1] = '\r'; + if (elen == sizeof(ethernet_command)-1) { + ethernet_command[sizeof(ethernet_command)-1] = '\r'; } - if (elen > 0 && eth_command[elen - 1] == '\r') { - eth_command[elen - 1] = 0; - eth_client.println(); + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); char reply[160]; reply[0] = 0; - if (!eth_handle_command(eth_command, reply)) - the_mesh.handleCommand(0, eth_command, reply); - if (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } - eth_client.print("> "); - eth_command[0] = 0; + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } + ethernet_client.print("> "); + ethernet_command[0] = 0; } } #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index a84a7ee93..97a793f49 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,43 +3,36 @@ #include "MyMesh.h" -#ifdef ETH_ENABLED +#ifdef ETHERNET_ENABLED #include #include + #include #define PIN_SPI1_MISO (29) #define PIN_SPI1_MOSI (30) #define PIN_SPI1_SCK (3) - SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_POWER_EN WB_IO2 #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 - #ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 23 + #ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 #endif - #define ETH_RETRY_INTERVAL_MS 30000 + #define ETHERNET_RETRY_INTERVAL_MS 30000 - static EthernetServer eth_server(ETH_TCP_PORT); - static EthernetClient eth_client; - static volatile bool eth_running = false; + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); + static EthernetClient ethernet_client; + static volatile bool ethernet_running = false; - static void generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; - } - - static void eth_task(void* param) { + static void ethernet_task(void* param) { (void)param; Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); vTaskDelay(pdMS_TO_TICKS(100)); pinMode(PIN_ETHERNET_RESET, OUTPUT); @@ -47,66 +40,66 @@ vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; generateDeviceMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - while (!eth_running) { + while (!ethernet_running) { if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("ETH: Hardware not found, giving up"); vTaskDelete(NULL); return; } if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } Serial.println("ETH: Link detected, attempting DHCP..."); if (Ethernet.begin(mac, 10000, 2000) == 0) { Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } IPAddress ip = Ethernet.localIP(); Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - Serial.printf("ETH: Listening on TCP port %d\n", ETH_TCP_PORT); - eth_server.begin(); - eth_running = true; + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; } vTaskDelete(NULL); } - static void eth_start_task() { - xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); } - static bool eth_handle_command(const char* command, char* reply) { + static bool ethernet_handle_command(const char* command, char* reply) { if (strcmp(command, "eth") != 0) return false; - if (!eth_running) { + if (!ethernet_running) { strcpy(reply, "ETH: not connected"); } else { IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); } return true; } - static void eth_check_client() { - if (eth_client && eth_client.connected()) return; - auto newClient = eth_server.available(); + static void ethernet_check_client() { + if (ethernet_client && ethernet_client.connected()) return; + auto newClient = ethernet_server.available(); if (newClient) { - if (eth_client) eth_client.stop(); - eth_client = newClient; - IPAddress ip = eth_client.remoteIP(); + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - eth_client.println("MeshCore Room Server CLI"); - eth_client.print("> "); + ethernet_client.println("MeshCore Room Server CLI"); + ethernet_client.print("> "); } } #endif @@ -125,8 +118,8 @@ void halt() { } static char command[MAX_POST_TEXT_LEN+1]; -#ifdef ETH_ENABLED -static char eth_command[MAX_POST_TEXT_LEN+1]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[MAX_POST_TEXT_LEN+1]; #endif void setup() { @@ -178,8 +171,8 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; -#ifdef ETH_ENABLED - eth_command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; #endif sensors.begin(); @@ -190,8 +183,8 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif -#ifdef ETH_ENABLED - eth_start_task(); +#ifdef ETHERNET_ENABLED + ethernet_start_task(); #endif // send out initial zero hop Advertisement to the mesh @@ -218,10 +211,13 @@ void loop() { command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; reply[0] = 0; -#ifdef ETH_ENABLED - if (!eth_handle_command(command, reply)) -#endif +#ifdef ETHERNET_ENABLED + if (!ethernet_handle_command(command, reply)) { + the_mesh.handleCommand(0, command, reply); + } +#else the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! +#endif if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -229,37 +225,38 @@ void loop() { command[0] = 0; // reset command buffer } -#ifdef ETH_ENABLED - if (eth_running) { - eth_check_client(); +#ifdef ETHERNET_ENABLED + if (ethernet_running) { + ethernet_check_client(); Ethernet.maintain(); } - if (eth_running && eth_client && eth_client.connected()) { - int elen = strlen(eth_command); - while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { - char c = eth_client.read(); - if (c == '\n') continue; - eth_command[elen++] = c; - eth_command[elen] = 0; - if (c == '\r') break; + if (ethernet_running && ethernet_client && ethernet_client.connected()) { + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; } - if (elen == sizeof(eth_command)-1) { - eth_command[sizeof(eth_command)-1] = '\r'; + if (elen == sizeof(ethernet_command)-1) { + ethernet_command[sizeof(ethernet_command)-1] = '\r'; } - if (elen > 0 && eth_command[elen - 1] == '\r') { - eth_command[elen - 1] = 0; - eth_client.println(); + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); char reply[160]; reply[0] = 0; - if (!eth_handle_command(eth_command, reply)) - the_mesh.handleCommand(0, eth_command, reply); - if (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } - eth_client.print("> "); - eth_command[0] = 0; + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } + ethernet_client.print("> "); + ethernet_command[0] = 0; } } #endif diff --git a/src/helpers/nrf52/EthernetMac.h b/src/helpers/nrf52/EthernetMac.h new file mode 100644 index 000000000..034acf704 --- /dev/null +++ b/src/helpers/nrf52/EthernetMac.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +static inline void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; + mac[1] = 0x92; + mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; +} diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 3dc206719..68f10b210 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -1,4 +1,5 @@ #include "SerialEthernetInterface.h" +#include "EthernetMac.h" #include #include @@ -6,12 +7,11 @@ #define PIN_SPI1_MOSI (30) // (0 + 30) #define PIN_SPI1_SCK (3) // (0 + 3) -SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); +SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); -#define PIN_ETH_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 -//#define STATIC_IP 1 #define RECV_STATE_IDLE 0 #define RECV_STATE_HDR_FOUND 1 @@ -19,15 +19,15 @@ SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); #define RECV_STATE_LEN2_FOUND 3 bool SerialEthernetInterface::begin() { - - ETH_DEBUG_PRINTLN("Ethernet initializing"); -#ifdef PIN_ETH_POWER_EN - ETH_DEBUG_PRINTLN("Ethernet power enable"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + +#ifdef PIN_ETHERNET_POWER_EN + ETHERNET_DEBUG_PRINTLN("Ethernet power enable"); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); // Power up. delay(100); - ETH_DEBUG_PRINTLN("Ethernet power enabled"); + ETHERNET_DEBUG_PRINTLN("Ethernet power enabled"); #endif #ifdef PIN_ETHERNET_RESET @@ -35,12 +35,12 @@ bool SerialEthernetInterface::begin() { digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. delay(100); digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. - ETH_DEBUG_PRINTLN("Ethernet reset pulse"); + ETHERNET_DEBUG_PRINTLN("Ethernet reset pulse"); #endif uint8_t mac[6]; generateDeviceMac(mac); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], @@ -48,49 +48,49 @@ bool SerialEthernetInterface::begin() { mac[3], mac[4], mac[5]); - ETH_DEBUG_PRINTLN("Init"); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + ETHERNET_DEBUG_PRINTLN("Init"); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - // Hardcode IP address for now - #ifdef STATIC_IP - IPAddress ip(192, 168, 8, 118); - IPAddress gateway(192, 168, 8, 1); - IPAddress subnet(255, 255, 255, 0); - IPAddress dns(192, 168, 8, 1); + // Use static IP if build flags are defined, otherwise DHCP + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gateway(ETHERNET_STATIC_GATEWAY); + IPAddress subnet(ETHERNET_STATIC_SUBNET); + IPAddress dns(ETHERNET_STATIC_DNS); Ethernet.begin(mac, ip, dns, gateway, subnet); #else - ETH_DEBUG_PRINTLN("Begin"); + ETHERNET_DEBUG_PRINTLN("Begin"); if (Ethernet.begin(mac) == 0) { - ETH_DEBUG_PRINTLN("Begin failed."); + ETHERNET_DEBUG_PRINTLN("Begin failed."); // DHCP failed -- let's figure out why if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. { - ETH_DEBUG_PRINTLN("Ethernet hardware not found."); + ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); return false; } if (Ethernet.linkStatus() == LinkOFF) // No physical connection { - ETH_DEBUG_PRINTLN("Ethernet cable not connected."); + ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); return false; } - ETH_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); return false; } #endif - ETH_DEBUG_PRINTLN("Ethernet begin complete"); + ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); IPAddress ip = Ethernet.localIP(); - ETH_DEBUG_PRINT_IP("IP", ip); - + ETHERNET_DEBUG_PRINT_IP("IP", ip); + IPAddress subnet = Ethernet.subnetMask(); - ETH_DEBUG_PRINT_IP("Subnet", subnet); - + ETHERNET_DEBUG_PRINT_IP("Subnet", subnet); + IPAddress gateway = Ethernet.gatewayIP(); - ETH_DEBUG_PRINT_IP("Gateway", gateway); + ETHERNET_DEBUG_PRINT_IP("Gateway", gateway); server.begin(); // start listening for clients - ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETH_TCP_PORT); + ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); return true; } @@ -108,13 +108,13 @@ void SerialEthernetInterface::disable() { size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { if (len > MAX_FRAME_SIZE) { - ETH_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); return 0; } if (deviceConnected && len > 0) { if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETH_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); return 0; } @@ -140,7 +140,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (newClient) { IPAddress new_ip = newClient.remoteIP(); uint16_t new_port = newClient.remotePort(); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "New client available %u.%u.%u.%u:%u", new_ip[0], new_ip[1], @@ -150,7 +150,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (client && client.connected()) { IPAddress cur_ip = client.remoteIP(); uint16_t cur_port = client.remotePort(); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Current client %u.%u.%u.%u:%u", cur_ip[0], cur_ip[1], @@ -158,27 +158,27 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { cur_ip[3], cur_port); if (cur_ip == new_ip && cur_port == new_port) { - ETH_DEBUG_PRINTLN("Ignoring duplicate client"); + ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); return 0; } } deviceConnected = false; if (client) { - ETH_DEBUG_PRINTLN("Closing previous client"); + ETHERNET_DEBUG_PRINTLN("Closing previous client"); client.stop(); } _state = RECV_STATE_IDLE; _frame_len = 0; _rx_len = 0; client = newClient; - ETH_DEBUG_PRINTLN("Switched to new client"); + ETHERNET_DEBUG_PRINTLN("Switched to new client"); } } if (client.connected()) { if (!deviceConnected) { - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Got connection %u.%u.%u.%u:%u", client.remoteIP()[0], client.remoteIP()[1], @@ -190,18 +190,18 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { } else { if (deviceConnected) { deviceConnected = false; - ETH_DEBUG_PRINTLN("Disconnected"); + ETHERNET_DEBUG_PRINTLN("Disconnected"); } } if (deviceConnected) { if (send_queue_len > 0) { // first, check send queue - + _last_write = millis(); int len = send_queue[0].len; -#if ETH_RAW_LINE - ETH_DEBUG_PRINTLN("TX line len=%d", len); +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); client.write(send_queue[0].buf, len); client.write("\r\n", 2); #else @@ -210,9 +210,9 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { pkt[1] = (len & 0xFF); // LSB pkt[2] = (len >> 8); // MSB memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETH_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETH_DEBUG_LOGGING && ARDUINO - ETH_DEBUG_PRINTLN("TX frame len=%d", len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); #endif client.write(pkt, 3 + len); #endif @@ -225,7 +225,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { int c = client.read(); if (c < 0) break; -#if ETH_RAW_LINE +#if ETHERNET_RAW_LINE if (c == '\r' || c == '\n') { if (_rx_len == 0) { continue; @@ -267,8 +267,8 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (_frame_len > MAX_FRAME_SIZE) { _frame_len = MAX_FRAME_SIZE; } - #if ETH_DEBUG_LOGGING && ARDUINO - ETH_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); #endif memcpy(dest, _rx_buf, _frame_len); _state = RECV_STATE_IDLE; @@ -284,20 +284,9 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { } bool SerialEthernetInterface::isConnected() const { - return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; + return deviceConnected; } -void SerialEthernetInterface::generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - - mac[0] = 0x02; - mac[1] = 0x92; - mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; -} - -void SerialEthernetInterface::maintain() { +void SerialEthernetInterface::loop() { Ethernet.maintain(); } diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 7adf3569e..5f06f688e 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -4,10 +4,10 @@ #include #include -#ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 5000 +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 #endif -// define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets class SerialEthernetInterface : public BaseSerialInterface { bool deviceConnected; @@ -44,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { protected: public: - SerialEthernetInterface() : server(EthernetServer(ETH_TCP_PORT)) { + SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { deviceConnected = false; _isEnabled = false; _last_write = 0; @@ -66,20 +66,17 @@ class SerialEthernetInterface : public BaseSerialInterface { size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; - void maintain(); - -private: - void generateDeviceMac(uint8_t mac[6]); + void loop(); }; -#if ETH_DEBUG_LOGGING && ARDUINO +#if ETHERNET_DEBUG_LOGGING && ARDUINO #include - #define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETH_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) #else - #define ETH_DEBUG_PRINT(...) {} - #define ETH_DEBUG_PRINTLN(...) {} - #define ETH_DEBUG_PRINT_IP(...) {} + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} #endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index ee09d31d5..55134e8c1 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -628,7 +628,7 @@ void EnvironmentSensorManager::rakGPSInit(){ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ - #if defined(ETH_ENABLED) && defined(RAK_BOARD) + #if defined(ETHERNET_ENABLED) && defined(RAK_BOARD) if (ioPin == WB_IO2) { // WB_IO2 powers the Ethernet module on RAK baseboards. return false; diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 96c8c73d2..cb280e31f 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -46,17 +46,17 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> -[env:RAK_4631_repeater_eth] +[env:RAK_4631_repeater_ethernet] extends = rak4631 build_flags = ${rak4631.build_flags} -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"RAK4631 Repeater ETH"' + -D ADVERT_NAME='"RAK4631 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + @@ -128,17 +128,17 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> -[env:RAK_4631_room_server_eth] +[env:RAK_4631_room_server_ethernet] extends = rak4631 build_flags = ${rak4631.build_flags} -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"Test Room ETH"' + -D ADVERT_NAME='"RAK4631 Room Server"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + @@ -170,7 +170,7 @@ lib_deps = densaugeo/base64 @ ~1.4.0 -[env:RAK_4631_companion_radio_eth] +[env:RAK_4631_companion_radio_ethernet] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6.ld board_upload.maximum_size = 712704 @@ -184,11 +184,10 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -; -D MESH_DEBUG=1 -; -D ETH_DEBUG_LOGGING=1 +; -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From 61ba79966b01b72476913f7783b2473b3ecb0652 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Wed, 11 Mar 2026 12:43:08 -0700 Subject: [PATCH 008/191] Address PR review feedback from liamcottle (second round) - Rename eth command to eth.status for consistency with other commands - Rename generateDeviceMac to generateEthernetMac for clarity - Refactor ethernet_handle_command to return false by default - Allow new TCP clients to replace existing connections (repeater, room server, SerialEthernetInterface) - Boot companion radio without Ethernet on init failure (LoRa-only recovery mode) - Remove > prompt from ethernet CLI for consistency with serial interface - Fix variable redeclaration compile error in SerialEthernetInterface when ETHERNET_STATIC_IP is defined - Fix TCP socket leak when duplicate client detection fires - Remove dead recv_queue and adv_restart_time members from SerialEthernetInterface - Fix port numbers in docs (port 23 for repeater/room server CLI, port 5000 for companion radio) - Clarify eth.status command is only available in repeater and room server firmware Co-Authored-By: Claude Sonnet 4.6 --- docs/cli_commands.md | 12 +-- docs/faq.md | 12 +-- examples/companion_radio/main.cpp | 10 ++- examples/simple_repeater/main.cpp | 24 +++-- examples/simple_room_server/main.cpp | 22 ++--- src/helpers/nrf52/EthernetMac.h | 2 +- src/helpers/nrf52/SerialEthernetInterface.cpp | 88 +++++++++---------- src/helpers/nrf52/SerialEthernetInterface.h | 6 +- 8 files changed, 82 insertions(+), 94 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ea2c941c2..18ddf59ef 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -885,22 +885,22 @@ region save ### Ethernet (when Ethernet support is compiled in) -Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_eth` firmware variants (e.g. `RAK_4631_repeater_eth`) to enable this feature. +Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature. --- #### View Ethernet connection status **Usage:** -- `eth` +- `eth.status` **Output:** -- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:5000`) +- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:23`) - `ETH: not connected` when Ethernet is not active **Notes:** +- Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI. - The Ethernet interface obtains an IP address via DHCP automatically on boot. -- A TCP server listens on port 5000 (default) for CLI connections. -- For repeaters and room servers, connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. -- For companion radio firmware, the Ethernet interface replaces BLE/USB as the transport to companion apps. +- A TCP server listens on port 23 (default) for CLI connections. +- Connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. --- diff --git a/docs/faq.md b/docs/faq.md index 84ab02261..8991508ff 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -892,14 +892,14 @@ MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.r **Firmware:** Flash one of the Ethernet-enabled firmware variants: -- `RAK_4631_repeater_eth` - Repeater with Ethernet CLI access -- `RAK_4631_room_server_eth` - Room server with Ethernet CLI access -- `RAK_4631_companion_radio_eth` - Companion radio over Ethernet (replaces BLE) +- `RAK_4631_repeater_ethernet` - Repeater with Ethernet CLI access +- `RAK_4631_room_server_ethernet` - Room server with Ethernet CLI access +- `RAK_4631_companion_radio_ethernet` - Companion radio over Ethernet (replaces BLE) **Connecting:** - The device obtains an IP address via DHCP automatically on boot. -- For repeaters and room servers, connect to the device on TCP port 5000 using any TCP client (e.g. `nc 5000` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. -- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. -- Use the `eth` CLI command to check connection status and see the assigned IP address. +- For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. `nc 23` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. +- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). +- Use the `eth.status` CLI command to check connection status and see the assigned IP address. --- diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9fa1e3810..d48e223d2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -160,6 +160,7 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + the_mesh.startInterface(serial_interface); #elif defined(ETHERNET_ENABLED) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); @@ -167,14 +168,15 @@ void setup() { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } Serial.println("Initializing Ethernet adapter..."); - if (!serial_interface.begin()) { - Serial.println("ETH: Init failed, halting"); - halt(); + if (serial_interface.begin()) { + the_mesh.startInterface(serial_interface); + } else { + Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); } #else serial_interface.begin(Serial); -#endif the_mesh.startInterface(serial_interface); +#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 0f37aa6b7..00fad1754 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -51,7 +51,7 @@ Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); @@ -92,20 +92,20 @@ // Format ethernet status into reply buffer. Returns true if command was handled. static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth") != 0) return false; - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; } - return true; + return false; } - // Check for new TCP client connections + // Check for new TCP client connections, replacing any existing connection static void ethernet_check_client() { - if (ethernet_client && ethernet_client.connected()) return; - auto newClient = ethernet_server.available(); if (newClient) { if (ethernet_client) ethernet_client.stop(); @@ -113,7 +113,6 @@ IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); ethernet_client.println("MeshCore Repeater CLI"); - ethernet_client.print("> "); } } #endif @@ -285,7 +284,6 @@ void loop() { if (reply[0]) { ethernet_client.print(" -> "); ethernet_client.println(reply); } - ethernet_client.print("> "); ethernet_command[0] = 0; } } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 97a793f49..435d56282 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -44,7 +44,7 @@ Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); @@ -80,18 +80,20 @@ } static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth") != 0) return false; - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; } - return true; + return false; } + // Check for new TCP client connections, replacing any existing connection static void ethernet_check_client() { - if (ethernet_client && ethernet_client.connected()) return; auto newClient = ethernet_server.available(); if (newClient) { if (ethernet_client) ethernet_client.stop(); @@ -99,7 +101,6 @@ IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); ethernet_client.println("MeshCore Room Server CLI"); - ethernet_client.print("> "); } } #endif @@ -255,7 +256,6 @@ void loop() { if (reply[0]) { ethernet_client.print(" -> "); ethernet_client.println(reply); } - ethernet_client.print("> "); ethernet_command[0] = 0; } } diff --git a/src/helpers/nrf52/EthernetMac.h b/src/helpers/nrf52/EthernetMac.h index 034acf704..0ee2ac06f 100644 --- a/src/helpers/nrf52/EthernetMac.h +++ b/src/helpers/nrf52/EthernetMac.h @@ -2,7 +2,7 @@ #include -static inline void generateDeviceMac(uint8_t mac[6]) { +static inline void generateEthernetMac(uint8_t mac[6]) { uint32_t device_id = NRF_FICR->DEVICEID[0]; mac[0] = 0x02; mac[1] = 0x92; diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 68f10b210..92001b1b2 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -39,7 +39,7 @@ bool SerialEthernetInterface::begin() { #endif uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); ETHERNET_DEBUG_PRINTLN( "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], @@ -80,14 +80,9 @@ bool SerialEthernetInterface::begin() { } #endif ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); - IPAddress ip = Ethernet.localIP(); - ETHERNET_DEBUG_PRINT_IP("IP", ip); - - IPAddress subnet = Ethernet.subnetMask(); - ETHERNET_DEBUG_PRINT_IP("Subnet", subnet); - - IPAddress gateway = Ethernet.gatewayIP(); - ETHERNET_DEBUG_PRINT_IP("Gateway", gateway); + ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); server.begin(); // start listening for clients ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); @@ -132,48 +127,45 @@ bool SerialEthernetInterface::isWriteBusy() const { } size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // check if new client connected - if (client && client.connected()) { - // Avoid polling for new clients while an active connection exists. - } else { - auto newClient = server.available(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); + // check if new client connected; new connections replace existing ones + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN( + "New client available %u.%u.%u.%u:%u", + new_ip[0], + new_ip[1], + new_ip[2], + new_ip[3], + new_port); + if (client && client.connected()) { + IPAddress cur_ip = client.remoteIP(); + uint16_t cur_port = client.remotePort(); ETHERNET_DEBUG_PRINTLN( - "New client available %u.%u.%u.%u:%u", - new_ip[0], - new_ip[1], - new_ip[2], - new_ip[3], - new_port); - if (client && client.connected()) { - IPAddress cur_ip = client.remoteIP(); - uint16_t cur_port = client.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "Current client %u.%u.%u.%u:%u", - cur_ip[0], - cur_ip[1], - cur_ip[2], - cur_ip[3], - cur_port); - if (cur_ip == new_ip && cur_port == new_port) { - ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); - return 0; - } + "Current client %u.%u.%u.%u:%u", + cur_ip[0], + cur_ip[1], + cur_ip[2], + cur_ip[3], + cur_port); + if (cur_ip == new_ip && cur_port == new_port) { + ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); + newClient.stop(); + return 0; } - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); } + + deviceConnected = false; + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETHERNET_DEBUG_PRINTLN("Switched to new client"); } if (client.connected()) { diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 5f06f688e..95ce8a521 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -13,7 +13,6 @@ class SerialEthernetInterface : public BaseSerialInterface { bool deviceConnected; bool _isEnabled; unsigned long _last_write; - unsigned long adv_restart_time; uint8_t _state; uint16_t _frame_len; uint16_t _rx_len; @@ -28,13 +27,10 @@ class SerialEthernetInterface : public BaseSerialInterface { }; #define FRAME_QUEUE_SIZE 4 - int recv_queue_len; - Frame recv_queue[FRAME_QUEUE_SIZE]; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { - recv_queue_len = 0; send_queue_len = 0; _state = 0; _frame_len = 0; @@ -48,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { deviceConnected = false; _isEnabled = false; _last_write = 0; - send_queue_len = recv_queue_len = 0; + send_queue_len = 0; _state = 0; _frame_len = 0; _rx_len = 0; From 88892de864b811fe286dedc510fb4e64f5955f7c Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Fri, 13 Mar 2026 10:48:42 -0700 Subject: [PATCH 009/191] Fix Ethernet init checking hardwareStatus before begin() and deduplicate CLI code The Ethernet retry loop in repeater and room server checked hardwareStatus() and linkStatus() before calling Ethernet.begin(), which always returned EthernetNoHardware since hardware detection only happens during begin(). Extract shared Ethernet CLI code into EthernetCLI.h to prevent future divergence. Also fix time_t type mismatch in companion radio Ethernet init. Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/main.cpp | 2 +- examples/simple_repeater/main.cpp | 146 ++----------------------- examples/simple_room_server/main.cpp | 139 ++---------------------- src/helpers/nrf52/EthernetCLI.h | 156 +++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 266 deletions(-) create mode 100644 src/helpers/nrf52/EthernetCLI.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d48e223d2..a88da48fc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -163,7 +163,7 @@ void setup() { the_mesh.startInterface(serial_interface); #elif defined(ETHERNET_ENABLED) Serial.print("Waiting for serial to connect...\n"); - time_t timeout = millis(); + unsigned long timeout = millis(); while (!Serial) { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 00fad1754..b9fa1aa89 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -9,112 +9,8 @@ #endif #ifdef ETHERNET_ENABLED - #include - #include - #include - - #define PIN_SPI1_MISO (29) - #define PIN_SPI1_MOSI (30) - #define PIN_SPI1_SCK (3) - SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - - #define PIN_ETHERNET_POWER_EN WB_IO2 - #define PIN_ETHERNET_RESET 21 - #define PIN_ETHERNET_SS 26 - - #ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 23 // telnet port for CLI access - #endif - - #define ETHERNET_RETRY_INTERVAL_MS 30000 - - static EthernetServer ethernet_server(ETHERNET_TCP_PORT); - static EthernetClient ethernet_client; - static volatile bool ethernet_running = false; - - // FreeRTOS task: handles hw init, DHCP, and retries in the background - static void ethernet_task(void* param) { - (void)param; - - // Hardware init - Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); - vTaskDelay(pdMS_TO_TICKS(100)); - - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); - digitalWrite(PIN_ETHERNET_RESET, HIGH); - - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - uint8_t mac[6]; - generateEthernetMac(mac); - Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - // Retry loop: keep trying until we get an IP - while (!ethernet_running) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - Serial.println("ETH: Hardware not found, giving up"); - vTaskDelete(NULL); - return; - } - - if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - Serial.println("ETH: Link detected, attempting DHCP..."); - if (Ethernet.begin(mac, 10000, 2000) == 0) { - Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - IPAddress ip = Ethernet.localIP(); - Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); - ethernet_server.begin(); - ethernet_running = true; - } - - // DHCP succeeded, task is done - vTaskDelete(NULL); - } - - static void ethernet_start_task() { - xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); - } - - // Format ethernet status into reply buffer. Returns true if command was handled. - static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth.status") == 0) { - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); - } - return true; - } - return false; - } - - // Check for new TCP client connections, replacing any existing connection - static void ethernet_check_client() { - auto newClient = ethernet_server.available(); - if (newClient) { - if (ethernet_client) ethernet_client.stop(); - ethernet_client = newClient; - IPAddress ip = ethernet_client.remoteIP(); - Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - ethernet_client.println("MeshCore Repeater CLI"); - } - } + #define ETHERNET_CLI_BANNER "MeshCore Repeater CLI" + #include #endif StdRNG fast_rng; @@ -255,37 +151,15 @@ void loop() { } #ifdef ETHERNET_ENABLED - if (ethernet_running) { - ethernet_check_client(); - Ethernet.maintain(); - } - - if (ethernet_running && ethernet_client && ethernet_client.connected()) { - int elen = strlen(ethernet_command); - while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { - char c = ethernet_client.read(); - if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) - if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } - ethernet_command[elen++] = c; - ethernet_command[elen] = 0; - } - if (elen == sizeof(ethernet_command)-1) { - ethernet_command[sizeof(ethernet_command)-1] = '\r'; - } - - if (elen > 0 && ethernet_command[elen - 1] == '\r') { - ethernet_command[elen - 1] = 0; - ethernet_client.println(); - char reply[160]; - reply[0] = 0; - if (!ethernet_handle_command(ethernet_command, reply)) { - the_mesh.handleCommand(0, ethernet_command, reply); - } - if (reply[0]) { - ethernet_client.print(" -> "); ethernet_client.println(reply); - } - ethernet_command[0] = 0; + ethernet_loop_maintain(); + if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { + char reply[160]; + reply[0] = 0; + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } + ethernet_send_reply(reply); + ethernet_command[0] = 0; } #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 435d56282..8439f500c 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -4,105 +4,8 @@ #include "MyMesh.h" #ifdef ETHERNET_ENABLED - #include - #include - #include - - #define PIN_SPI1_MISO (29) - #define PIN_SPI1_MOSI (30) - #define PIN_SPI1_SCK (3) - SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - - #define PIN_ETHERNET_POWER_EN WB_IO2 - #define PIN_ETHERNET_RESET 21 - #define PIN_ETHERNET_SS 26 - - #ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 23 - #endif - - #define ETHERNET_RETRY_INTERVAL_MS 30000 - - static EthernetServer ethernet_server(ETHERNET_TCP_PORT); - static EthernetClient ethernet_client; - static volatile bool ethernet_running = false; - - static void ethernet_task(void* param) { - (void)param; - - Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); - vTaskDelay(pdMS_TO_TICKS(100)); - - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); - digitalWrite(PIN_ETHERNET_RESET, HIGH); - - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - uint8_t mac[6]; - generateEthernetMac(mac); - Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - while (!ethernet_running) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - Serial.println("ETH: Hardware not found, giving up"); - vTaskDelete(NULL); - return; - } - if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - Serial.println("ETH: Link detected, attempting DHCP..."); - if (Ethernet.begin(mac, 10000, 2000) == 0) { - Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - IPAddress ip = Ethernet.localIP(); - Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); - ethernet_server.begin(); - ethernet_running = true; - } - vTaskDelete(NULL); - } - - static void ethernet_start_task() { - xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); - } - - static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth.status") == 0) { - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); - } - return true; - } - return false; - } - - // Check for new TCP client connections, replacing any existing connection - static void ethernet_check_client() { - auto newClient = ethernet_server.available(); - if (newClient) { - if (ethernet_client) ethernet_client.stop(); - ethernet_client = newClient; - IPAddress ip = ethernet_client.remoteIP(); - Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - ethernet_client.println("MeshCore Room Server CLI"); - } - } + #define ETHERNET_CLI_BANNER "MeshCore Room Server CLI" + #include #endif #ifdef DISPLAY_CLASS @@ -227,37 +130,15 @@ void loop() { } #ifdef ETHERNET_ENABLED - if (ethernet_running) { - ethernet_check_client(); - Ethernet.maintain(); - } - - if (ethernet_running && ethernet_client && ethernet_client.connected()) { - int elen = strlen(ethernet_command); - while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { - char c = ethernet_client.read(); - if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) - if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } - ethernet_command[elen++] = c; - ethernet_command[elen] = 0; - } - if (elen == sizeof(ethernet_command)-1) { - ethernet_command[sizeof(ethernet_command)-1] = '\r'; - } - - if (elen > 0 && ethernet_command[elen - 1] == '\r') { - ethernet_command[elen - 1] = 0; - ethernet_client.println(); - char reply[160]; - reply[0] = 0; - if (!ethernet_handle_command(ethernet_command, reply)) { - the_mesh.handleCommand(0, ethernet_command, reply); - } - if (reply[0]) { - ethernet_client.print(" -> "); ethernet_client.println(reply); - } - ethernet_command[0] = 0; + ethernet_loop_maintain(); + if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { + char reply[160]; + reply[0] = 0; + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } + ethernet_send_reply(reply); + ethernet_command[0] = 0; } #endif diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h new file mode 100644 index 000000000..6a59bda68 --- /dev/null +++ b/src/helpers/nrf52/EthernetCLI.h @@ -0,0 +1,156 @@ +#pragma once + +#ifdef ETHERNET_ENABLED + +#include +#include +#include +#include + +#define PIN_SPI1_MISO (29) +#define PIN_SPI1_MOSI (30) +#define PIN_SPI1_SCK (3) + +static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETHERNET_POWER_EN WB_IO2 +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 // telnet port for CLI access +#endif + +#ifndef ETHERNET_CLI_BANNER + #define ETHERNET_CLI_BANNER "MeshCore CLI" +#endif + +#define ETHERNET_RETRY_INTERVAL_MS 30000 + +static EthernetServer ethernet_server(ETHERNET_TCP_PORT); +static EthernetClient ethernet_client; +static volatile bool ethernet_running = false; + +// FreeRTOS task: handles hw init, DHCP, and retries in the background +static void ethernet_task(void* param) { + (void)param; + + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateEthernetMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + // Retry loop: keep trying until we get an IP + while (!ethernet_running) { + Serial.println("ETH: Attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + if (Ethernet.linkStatus() == LinkOFF) { + Serial.println("ETH: Cable not connected, will retry"); + } else { + Serial.println("ETH: DHCP failed, will retry"); + } + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); + continue; + } + + IPAddress ip = Ethernet.localIP(); + Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; + } + + // DHCP succeeded, task is done + vTaskDelete(NULL); +} + +static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); +} + +// Format ethernet status into reply buffer. Returns true if command was handled. +static bool ethernet_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; + } + return false; +} + +// Check for new TCP client connections, replacing any existing connection +static void ethernet_check_client() { + auto newClient = ethernet_server.available(); + if (newClient) { + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + ethernet_client.println(ETHERNET_CLI_BANNER); + } +} + +// Call from loop() to maintain DHCP and check for new clients +static void ethernet_loop_maintain() { + if (ethernet_running) { + ethernet_check_client(); + Ethernet.maintain(); + } +} + +// Read a line from the Ethernet client into the command buffer. +// Returns true when a complete line is ready to process (command is null-terminated). +// The caller should process the command and then reset ethernet_command[0] = 0. +static bool ethernet_read_line(char* ethernet_command, size_t buf_size) { + if (!ethernet_running || !ethernet_client || !ethernet_client.connected()) return false; + + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)buf_size - 1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; + } + if (elen == (int)buf_size - 1) { + ethernet_command[buf_size - 1] = '\r'; + } + + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); + return true; + } + return false; +} + +// Send a reply to the Ethernet client +static void ethernet_send_reply(const char* reply) { + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } +} + +#endif // ETHERNET_ENABLED From 3c1a0941aa56cc733cacb385d7784cafb78f736e Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Fri, 20 Mar 2026 17:56:04 -0700 Subject: [PATCH 010/191] Fix POE boot failure on RAK4631 Ethernet builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive WB_IO2 (slot power switch) HIGH via early constructor before the Arduino framework initializes. Without this, the board brownouts on POE-only power because the slot MOSFET isn't enabled early enough for the RAK13800 POE module to deliver power to the baseboard. Also remove the W5100S hardware reset pulse from Ethernet init — the chip comes out of power-on reset cleanly, and toggling reset kills the PHY link which breaks POE power delivery. --- src/helpers/nrf52/EthernetCLI.h | 10 ++++------ src/helpers/nrf52/SerialEthernetInterface.cpp | 17 +++++------------ variants/rak4631/RAK4631Board.cpp | 13 +++++++++++++ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 6a59bda68..9ccc34f49 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -36,13 +36,11 @@ static void ethernet_task(void* param) { (void)param; Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); - vTaskDelay(pdMS_TO_TICKS(100)); - + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); ETHERNET_SPI_PORT.begin(); diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 92001b1b2..4288c0f7d 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -22,20 +22,13 @@ bool SerialEthernetInterface::begin() { ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); -#ifdef PIN_ETHERNET_POWER_EN - ETHERNET_DEBUG_PRINTLN("Ethernet power enable"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); // Power up. - delay(100); - ETHERNET_DEBUG_PRINTLN("Ethernet power enabled"); -#endif - + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. #ifdef PIN_ETHERNET_RESET pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. - delay(100); - digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. - ETHERNET_DEBUG_PRINTLN("Ethernet reset pulse"); + digitalWrite(PIN_ETHERNET_RESET, HIGH); #endif uint8_t mac[6]; diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 08286604c..1b5698d0e 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -1,8 +1,21 @@ #include #include +#include "nrf_gpio.h" #include "RAK4631Board.h" +#ifdef ETHERNET_ENABLED +// Drive WB_IO2 HIGH as early as possible using direct register access. +// WB_IO2 (P1.02, Arduino pin 34) controls the WisBlock slot power switch. +// With POE through RAK13800, this must be enabled before the framework +// initializes or the board will brownout from insufficient power delivery. +// Priority 102 runs just after SystemInit. +static void __attribute__((constructor(102))) rak4631_early_poe_power() { + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 2)); // WB_IO2 = P1.02 + nrf_gpio_pin_set(NRF_GPIO_PIN_MAP(1, 2)); +} +#endif + #ifdef NRF52_POWER_MANAGEMENT // Static configuration for power management // Values set in variant.h defines From 0ffe1c2cb9fa9fcb14b8c577dee39a04176eff5c Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sat, 21 Mar 2026 19:28:44 -0700 Subject: [PATCH 011/191] bug fix: ethernet cli dropping connection after each command --- src/helpers/nrf52/EthernetCLI.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 9ccc34f49..508d3aa60 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -102,6 +102,8 @@ static bool ethernet_handle_command(const char* command, char* reply) { static void ethernet_check_client() { auto newClient = ethernet_server.available(); if (newClient) { + // Only replace if this is actually a different client + if (newClient == ethernet_client && ethernet_client.connected()) return; if (ethernet_client) ethernet_client.stop(); ethernet_client = newClient; IPAddress ip = ethernet_client.remoteIP(); From 8435464c84fd86602f0b38bf77d33d7e065080e9 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 24 Mar 2026 10:48:15 +0800 Subject: [PATCH 012/191] 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 9769d7133..4dbf84e10 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 40f1ceeb6..98a7a0dcb 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 b94e45267..d5d0abfed 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 557be306c..7ecfdf7da 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 24e889492..92affc5a8 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 7b9437738..7f61c2668 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 57d23a311..58490fd0f 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 70cd0f067..91b99c80e 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -57,6 +57,9 @@ public: virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } + virtual bool canControlLoRaFemLna() const { return false; } + virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } @@ -100,4 +103,4 @@ public: } }; -} \ No newline at end of file +} diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 8b097c29f..1c2afd371 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 3a4332d1f..82d5fcda5 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 49580d2ec..4c79825a7 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 4d5ee4615..fe77caed0 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -19,5 +19,8 @@ public: 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 754529650..d84ebe9c6 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 013/191] 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 d096d14b2..d1eae208e 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 46311c5eb..a48cc6b30 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 0694bc3c1..e16c82660 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 014/191] 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 aabfed796..f182c905e 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 33c897bc9..ccbecc7ab 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -21,5 +21,8 @@ public: 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 2c50b7428..0ce60fffd 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 015/191] 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 2c50b7428..0ce60fffd 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 550131571..54425145c 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 d1e3bdfde..15c7e68b5 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,4 +25,7 @@ public: 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 016/191] 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 f8b9e5e08..ebad1f6f5 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 3edc0a695..9fe1534a1 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 017/191] 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 91da9bccf..e98a78733 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 b9152f4cd..d22e08a8f 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 b2caaa354..fc37b9f6a 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -25,7 +25,6 @@ public: 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 018/191] 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 e98a78733..1b664eead 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 019/191] 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 1b664eead..2d1f6dcd4 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 a3354db5214d9ea60c16671c5682b69d4d098fd5 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sun, 26 Apr 2026 20:22:35 +0000 Subject: [PATCH 020/191] Address PR review feedback from oltaco - Remove extra blank line in companion_radio main.cpp before NRF52/STM32 block - Revert unintended STM32_PLATFORM ETHERNET_ENABLED branch (no STM32 ethernet target exists, and it incorrectly included nrf52 headers) - Drop renovate annotations from rak4631 platformio.ini (no renovate config in this repo) --- examples/companion_radio/main.cpp | 10 ++-------- variants/rak4631/platformio.ini | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 822100c94..61df6c0b0 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,7 +12,6 @@ static uint32_t _atoi(const char* sp) { return n; } - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -83,13 +82,8 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #ifdef ETHERNET_ENABLED - #include - SerialEthernetInterface serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif + #include + ArduinoSerialInterface serial_interface; #else #error "need to define a serial interface" #endif diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 7a0b031f0..d24678f44 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -65,7 +65,6 @@ build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_repeater> lib_deps = ${rak4631.lib_deps} - # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip [env:RAK_4631_repeater_bridge_rs232_serial1] @@ -147,7 +146,6 @@ build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_room_server> lib_deps = ${rak4631.lib_deps} - # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip [env:RAK_4631_companion_radio_usb] @@ -197,7 +195,6 @@ build_src_filter = ${rak4631.build_src_filter} lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 - # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip 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 021/191] 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 f1dc678ea..362ba68a3 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 022/191] 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 f636c32ab..c4e318d51 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 9ef8abaa6..cb4f49236 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 1bc7e7fa7..38594ecc2 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 a40b8ce99..96411c55f 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 023/191] 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 7b8399e26..36c1d19d5 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 024/191] 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 000000000..9f9b6ab59 --- /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 000000000..2fd7a6182 --- /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 000000000..817d3c1a4 --- /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 000000000..2fac9fbc3 --- /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 000000000..ad4573548 --- /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 000000000..037192461 --- /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 000000000..699c6e4af --- /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 000000000..d3f2599a0 --- /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 c183050935f3ca5e589c7759e14262be9cf24f25 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 15 May 2026 16:53:18 +0800 Subject: [PATCH 025/191] Add heltec -v4-r8 board --- boards/heltec_v4_r8.json | 43 +++ src/helpers/ui/ST7789LCDDisplay.cpp | 8 +- src/helpers/ui/ST7789LCDDisplay.h | 4 +- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 82 ++++++ variants/heltec_v4_r8/HeltecV4R8Board.h | 39 +++ variants/heltec_v4_r8/LoRaFEMControl.cpp | 52 ++++ variants/heltec_v4_r8/LoRaFEMControl.h | 24 ++ variants/heltec_v4_r8/pins_arduino.h | 56 ++++ variants/heltec_v4_r8/platformio.ini | 342 ++++++++++++++++++++++ variants/heltec_v4_r8/target.cpp | 45 +++ variants/heltec_v4_r8/target.h | 31 ++ 11 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 boards/heltec_v4_r8.json create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.cpp create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.h create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.cpp create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.h create mode 100644 variants/heltec_v4_r8/pins_arduino.h create mode 100644 variants/heltec_v4_r8/platformio.ini create mode 100644 variants/heltec_v4_r8/target.cpp create mode 100644 variants/heltec_v4_r8/target.h diff --git a/boards/heltec_v4_r8.json b/boards/heltec_v4_r8.json new file mode 100644 index 000000000..6dd97c84b --- /dev/null +++ b/boards/heltec_v4_r8.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_v4_r8" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index dc75e9631..7a02668bc 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -1,5 +1,9 @@ #include "ST7789LCDDisplay.h" +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 3 #endif @@ -29,8 +33,8 @@ bool ST7789LCDDisplay::begin() { } // Im not sure if this is just a t-deck problem or not, if your display is slow try this. - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) - displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS); + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) + displaySPI.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); #endif display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT); diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index 5b960ca19..03a6d3f12 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -8,7 +8,7 @@ #include class ST7789LCDDisplay : public DisplayDriver { - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) SPIClass displaySPI; #endif Adafruit_ST7789 display; @@ -25,7 +25,7 @@ public: { _isOn = false; } -#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) +#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), displaySPI(HSPI), display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp new file mode 100644 index 000000000..1d481fcc7 --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -0,0 +1,82 @@ +#include "HeltecV4R8Board.h" + +void HeltecV4R8Board::begin() { + ESP32Board::begin(); + + periph_power.begin(); + periph_power.claim(); // R8 VEXT also feeds the LoRa antenna boost rail. + + loRaFEMControl.init(); + +#ifdef PIN_TOUCH_RST + pinMode(PIN_TOUCH_RST, OUTPUT); + digitalWrite(PIN_TOUCH_RST, HIGH); +#endif + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecV4R8Board::onBeforeTransmit(void) { + digitalWrite(P_LORA_TX_LED, HIGH); + loRaFEMControl.setTxModeEnable(); +} + +void HeltecV4R8Board::onAfterTransmit(void) { + digitalWrite(P_LORA_TX_LED, LOW); + loRaFEMControl.setRxModeEnable(); +} + +void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + 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); + loRaFEMControl.setRxModeEnableWhenMCUSleep(); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + esp_deep_sleep_start(); +} + +void HeltecV4R8Board::powerOff() { + enterDeepSleep(0); +} + +uint16_t HeltecV4R8Board::getBattMilliVolts() { + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; +} + +const char* HeltecV4R8Board::getManufacturerName() const { +#ifdef HELTEC_V4_R8_TFT + return "Heltec V4 R8 TFT"; +#else + return "Heltec V4 R8 OLED"; +#endif +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h new file mode 100644 index 000000000..20811abb1 --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include "LoRaFEMControl.h" + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER (4.9f * 1.035f) +#endif + +class HeltecV4R8Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + LoRaFEMControl loRaFEMControl; + + HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + 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; + bool setAdcMultiplier(float multiplier) override { + if (multiplier == 0.0f) { + adc_mult = ADC_MULTIPLIER; + } else { + adc_mult = multiplier; + } + return true; + } + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_v4_r8/LoRaFEMControl.cpp b/variants/heltec_v4_r8/LoRaFEMControl.cpp new file mode 100644 index 000000000..bb530de3c --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.cpp @@ -0,0 +1,52 @@ +#include "LoRaFEMControl.h" + +#include +#include +#include + +void LoRaFEMControl::init(void) { + pinMode(P_LORA_PA_POWER, OUTPUT); + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason != ESP_RST_DEEPSLEEP) { + delay(1); + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CTX); + + pinMode(P_LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setSleepModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, LOW); +} + +void LoRaFEMControl::setTxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH); +} + +void LoRaFEMControl::setRxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setRxModeEnableWhenMCUSleep(void) { + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CTX); +} + +void LoRaFEMControl::setLNAEnable(bool enabled) { + lna_enabled = enabled; +} diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h new file mode 100644 index 000000000..961cfd070 --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -0,0 +1,24 @@ +#pragma once + +typedef enum { + KCT8103L_PA, + OTHER_FEM_TYPES +} LoRaFEMType; + +class LoRaFEMControl { +public: + LoRaFEMControl() { } + virtual ~LoRaFEMControl() { } + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + bool isLnaCanControl(void) { return true; } + void setLnaCanControl(bool can_control) { } + LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } + +private: + bool lna_enabled = false; +}; diff --git a/variants/heltec_v4_r8/pins_arduino.h b/variants/heltec_v4_r8/pins_arduino.h new file mode 100644 index 000000000..9e412aac5 --- /dev/null +++ b/variants/heltec_v4_r8/pins_arduino.h @@ -0,0 +1,56 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 17; +static const uint8_t SCL = 18; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +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 diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini new file mode 100644 index 000000000..4057d6f1b --- /dev/null +++ b/variants/heltec_v4_r8/platformio.ini @@ -0,0 +1,342 @@ +[Heltec_v4_r8] +extends = esp32_base +board = heltec_v4_r8 +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_v4_r8 + -D HELTEC_V4_R8 + -D USE_SX1262 + -D ESP32_CPU_FREQ=80 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=8 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=9 + -D P_LORA_MISO=11 + -D P_LORA_MOSI=10 + -D P_LORA_PA_POWER=7 + -D P_LORA_KCT8103L_PA_CSD=2 + -D P_LORA_KCT8103L_PA_CTX=5 + -D P_LORA_TX_LED=46 + -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=40 + -D PIN_VEXT_EN_ACTIVE=LOW + -D ADC_MULTIPLIER=5.0715f + -D PIN_VBAT_READ=1 + -D LORA_TX_POWER=10 + -D MAX_LORA_TX_POWER=22 + -D SX126X_REGISTER_PATCH=1 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_GPS_RX=38 + -D PIN_GPS_TX=39 + -D PIN_GPS_EN=42 + -D PIN_GPS_EN_ACTIVE=LOW + -D ENV_INCLUDE_GPS=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_v4_r8> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[heltec_v4_r8_oled] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_OLED + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D PIN_OLED_RESET=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} +lib_deps = ${Heltec_v4_r8.lib_deps} + +[heltec_v4_r8_tft] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_TFT + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D DISPLAY_SCALE_X=2.5 + -D DISPLAY_SCALE_Y=3.75 + -D PIN_TFT_RST=-1 + -D PIN_TFT_VDD_CTL=-1 + -D PIN_TFT_LEDA_CTL=44 + -D PIN_TFT_LEDA_CTL_ACTIVE=HIGH + -D PIN_TFT_CS=47 + -D PIN_TFT_DC=48 + -D PIN_TFT_SCL=16 + -D PIN_TFT_SDA=15 + -D PIN_TFT_MISO=45 + -D PIN_BUZZER=4 + -D PIN_TOUCH_RST=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + + +lib_deps = + ${Heltec_v4_r8.lib_deps} + adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_v4_r8_repeater] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_room_server] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_terminal_chat] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_usb] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_ble] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_wifi] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SSD1306Display + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_sensor] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_repeater] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_tft_room_server] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_terminal_chat] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_usb] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_ble] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=ST7789LCDDisplay + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_wifi] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=ST7789LCDDisplay + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_sensor] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_kiss_modem] +extends = Heltec_v4_r8 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + +<../examples/kiss_modem/> + +[env:heltec_v4_r8_tft_kiss_modem] +extends = heltec_v4_r8_tft +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_v4_r8/target.cpp b/variants/heltec_v4_r8/target.cpp new file mode 100644 index 000000000..0b38531e3 --- /dev/null +++ b/variants/heltec_v4_r8/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecV4R8Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display(&board.periph_power); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_v4_r8/target.h b/variants/heltec_v4_r8/target.h new file mode 100644 index 000000000..2d1d7a5bb --- /dev/null +++ b/variants/heltec_v4_r8/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #ifdef HELTEC_V4_R8_OLED + #include + #elif defined(HELTEC_V4_R8_TFT) + #include + #endif + #include +#endif + +extern HeltecV4R8Board 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(); From f29cae602f0f2e1102fb085023202ad6861620e1 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 16 May 2026 09:30:49 +0700 Subject: [PATCH 026/191] 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 9f504b8ed..0d8c2a79d 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 1ac5f359ca572141ca0fa297a9540656a959d842 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 16 May 2026 10:59:02 +0800 Subject: [PATCH 027/191] Added touch reset --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1d481fcc7..f9a771df4 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -11,6 +11,10 @@ void HeltecV4R8Board::begin() { #ifdef PIN_TOUCH_RST pinMode(PIN_TOUCH_RST, OUTPUT); digitalWrite(PIN_TOUCH_RST, HIGH); + delay(10); + digitalWrite(PIN_TOUCH_RST, LOW); + delay(100); + digitalWrite(PIN_TOUCH_RST, HIGH); #endif esp_reset_reason_t reason = esp_reset_reason(); From 40bd7d511b18cf1a94f0a5601666eb822f7f9541 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 18 May 2026 21:04:09 +0000 Subject: [PATCH 028/191] Fix companion disconnect on inbound packet; use server.accept() EthernetServer::available() returns any socket with data, including the already-connected client's socket. The companion path then called stop() on what it thought was a duplicate client, closing the shared socket and disconnecting the companion after the first packet. Switch both SerialEthernetInterface and EthernetCLI to server.accept(), which only returns newly-accepted sockets. Removes the IP/port duplicate detection in the companion path (no longer reachable) and the same-socket early-return in the CLI path. Reported by mcode6726 on PR #1983. --- src/helpers/nrf52/EthernetCLI.h | 9 ++++--- src/helpers/nrf52/SerialEthernetInterface.cpp | 25 +++++-------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 508d3aa60..34802e567 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -98,12 +98,13 @@ static bool ethernet_handle_command(const char* command, char* reply) { return false; } -// Check for new TCP client connections, replacing any existing connection +// Check for new TCP client connections, replacing any existing connection. +// Use accept() (not available()) so we only see newly-accepted sockets; +// available() also returns existing connected sockets that have data, which +// would force us to disambiguate every inbound packet from a real new client. static void ethernet_check_client() { - auto newClient = ethernet_server.available(); + auto newClient = ethernet_server.accept(); if (newClient) { - // Only replace if this is actually a different client - if (newClient == ethernet_client && ethernet_client.connected()) return; if (ethernet_client) ethernet_client.stop(); ethernet_client = newClient; IPAddress ip = ethernet_client.remoteIP(); diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 4288c0f7d..708910232 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -120,34 +120,21 @@ bool SerialEthernetInterface::isWriteBusy() const { } size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // check if new client connected; new connections replace existing ones - auto newClient = server.available(); + // Use accept() (not available()) so we only see newly-accepted sockets. + // available() also returns existing connected sockets that have data, + // which would cause us to treat each inbound packet as a "new client" + // and stop() the underlying socket — disconnecting the companion. + auto newClient = server.accept(); if (newClient) { IPAddress new_ip = newClient.remoteIP(); uint16_t new_port = newClient.remotePort(); ETHERNET_DEBUG_PRINTLN( - "New client available %u.%u.%u.%u:%u", + "New client accepted %u.%u.%u.%u:%u", new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); - if (client && client.connected()) { - IPAddress cur_ip = client.remoteIP(); - uint16_t cur_port = client.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "Current client %u.%u.%u.%u:%u", - cur_ip[0], - cur_ip[1], - cur_ip[2], - cur_ip[3], - cur_port); - if (cur_ip == new_ip && cur_port == new_port) { - ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); - newClient.stop(); - return 0; - } - } deviceConnected = false; if (client) { From e5a3839d64192c984e2fa5c76450c77bd06dc36e Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 27 May 2026 15:12:59 +0800 Subject: [PATCH 029/191] Optimize ADC readout --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index f9a771df4..1fb123b2f 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -70,11 +70,11 @@ uint16_t HeltecV4R8Board::getBattMilliVolts() { uint32_t raw = 0; for (int i = 0; i < 8; i++) { - raw += analogRead(PIN_VBAT_READ); + raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; - return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; + return (adc_mult * raw); } const char* HeltecV4R8Board::getManufacturerName() const { From 00d445a269451e2af0bd54365346c9a8b584f300 Mon Sep 17 00:00:00 2001 From: Sefinek Date: Thu, 28 May 2026 15:44:56 +0200 Subject: [PATCH 030/191] 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 c7988bb34..dda0a84ec 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 d53d6d25f..d3c1f73ce 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -100,7 +100,7 @@ protected: * \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 0886a06c4..c19d9e9d8 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 b9eb1c105..808e9df43 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 dc032eb96..6a7d7064e 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 191013440..aa11ce1a8 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 031/191] 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 d3c1f73ce..a4967c1d7 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -212,12 +212,12 @@ public: 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 37b50f3f1..70a036c49 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -142,7 +142,7 @@ public: 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 032/191] 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 6fbb0f742..073539886 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 033/191] 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 073539886..c468967f9 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 034/191] 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 2fd7a6182..04213bda8 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 817d3c1a4..6912acdec 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 @@ protected: #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 000000000..d87d1849e --- /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 000000000..e6c0ad2f1 --- /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 2fac9fbc3..190a21c90 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 699c6e4af..66ec2111c 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 d3f2599a0..352184b86 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 035/191] 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 190a21c90..f029b7d41 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 036/191] 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 66ec2111c..cfb88c4a3 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 037/191] * 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 87ad61af2..e9b92262c 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 038/191] 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 fdb924ad4..bf2f36c3d 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 1dd5162bf..c468967f9 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 ecb117bd2..84d51413b 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 039/191] 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 84d51413b..48c381cea 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 040/191] * 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 c468967f9..d05bc6de5 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 d50667368..d93810ed9 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 d3ef034e6..71b2681f4 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 c04bfda38..3a277c1ea 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -28,8 +28,9 @@ public: 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 @@ protected: } 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 041/191] * 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 71b2681f4..ef5863030 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 042/191] * 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 ef5863030..972a97e9e 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 3a277c1ea..d98785470 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -172,7 +172,8 @@ public: 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 043/191] * 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 d05bc6de5..0b90df8bd 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 044/191] 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 5a5d1eabd..25f1a2d19 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 a4cbf2a98..1efc99f36 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -155,6 +155,42 @@ public: 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 045/191] 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 0b90df8bd..2433f595e 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 af4706c13..dd282ec81 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 349efb445..97ec80ac5 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 c0235a143..2a2c7febc 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 b6519aefa..c67ab768c 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 efd3e1793..c1ece6448 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -32,7 +32,7 @@ public: bool isInRecvMode() const override; bool isChannelActive(); - bool isReceiving() override { + bool isReceiving() override { if (isReceivingPacket()) return true; return isChannelActive(); @@ -46,6 +46,7 @@ public: 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 046/191] 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 c67ab768c..9b00b176b 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 047/191] 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 9b00b176b..d50c2bace 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 048/191] 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 d50c2bace..5d68d7e90 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 049/191] 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 ce0e7da18..c06f5e12b 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 2433f595e..5fb9bf9d3 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 43d3950be..f4190f30a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -105,6 +105,7 @@ public: 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 dd282ec81..5cc3a9a11 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 7597c6c6f..24c4b1f2a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -150,6 +150,9 @@ protected: 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 97ec80ac5..12d0b0c31 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 5277ddad6..e9e53ec91 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -144,6 +144,9 @@ protected: 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 2a2c7febc..59c9aa090 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 c9f135f65..1d65b8772 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -120,6 +120,7 @@ protected: 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 9d7a11131..c0610b7f8 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 dd032f130..aad6cba3e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -65,6 +65,8 @@ public: virtual void triggerNoiseFloorCalibrate(int threshold) { } + virtual void setCADEnabled(bool enable) { } + virtual void resetAGC() { } virtual bool isInRecvMode() const = 0; @@ -166,6 +168,7 @@ protected: 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 5a5d1eabd..82e537435 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 ffeadc5bd..10cb00c77 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 5d68d7e90..5e72336c0 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; - 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; + // 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; + } + + return false; } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index c1ece6448..9943bcab7 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -9,6 +9,7 @@ protected: 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 @@ public: 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 050/191] 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 92d641a35..bd0423228 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 52a0d8352..0d2a74adf 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 051/191] 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 cb8f75db5..90a902f80 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 b8b6598dd3ab47489767167aa85ce727c5823054 Mon Sep 17 00:00:00 2001 From: UltimateCodeWarrior Date: Mon, 22 Jun 2026 23:30:35 -0700 Subject: [PATCH 052/191] Fix blank OLED on T-Beam Supreme V3 (I2C bus + 0x3D address) The SH1106 OLED on the LilyGo T-Beam Supreme V3 never lights up because: 1. The primary I2C bus (Wire) that the OLED, BME280 and magnetometer share is never initialized. The PMU/RTC use Wire1 (via XPowersLib); nothing brings up Wire on PIN_BOARD_SDA/PIN_BOARD_SCL, so the display driver talks on the wrong default pins. 2. The OLED is at I2C address 0x3D on this board (0x3C is taken by the QMC6310 magnetometer), but SH1106Display::begin() only tried 0x3C. Initialize Wire for TBEAM_SUPREME_SX1262, and have SH1106Display::begin() prefer 0x3D (only ever used by the OLED) before falling back to 0x3C. Backwards-compatible for boards with the OLED at 0x3C. Closes #2609 --- src/helpers/esp32/TBeamBoard.cpp | 10 ++++++++++ src/helpers/ui/SH1106Display.cpp | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/helpers/esp32/TBeamBoard.cpp b/src/helpers/esp32/TBeamBoard.cpp index 5f708d71d..d9f6e0227 100644 --- a/src/helpers/esp32/TBeamBoard.cpp +++ b/src/helpers/esp32/TBeamBoard.cpp @@ -16,6 +16,16 @@ void TBeamBoard::begin() { ESP32Board::begin(); +#ifdef TBEAM_SUPREME_SX1262 + // On the T-Beam S3 Supreme the PMU + RTC sit on Wire1 (GPIO 42/41, brought + // up by XPowersLib), while the SH1106 OLED and the BME280/QMC6310 sensors + // sit on the primary bus, Wire (GPIO PIN_BOARD_SDA/PIN_BOARD_SCL). Nothing + // else initialises Wire on this board, so the display driver would + // otherwise talk on the wrong (default) pins and display.begin() fails, + // leaving the screen blank. Bring the OLED bus up here. + Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + power_init(); //Configure user button diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index f383bb005..86c6089bc 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,7 +11,17 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + // Address selection: on some board revisions (notably the LilyGo T-Beam + // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a + // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it + // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). + uint8_t addr = 0; + if (i2c_probe(Wire, 0x3D)) { + addr = 0x3D; + } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { + addr = DISPLAY_ADDRESS; + } + return addr && display.begin(addr, true); } void SH1106Display::turnOn() From 4ac8742ce0bd9d09fbd93cb51681aea7e1b738bb Mon Sep 17 00:00:00 2001 From: Nick Dunklee Date: Tue, 23 Jun 2026 11:28:55 -0600 Subject: [PATCH 053/191] feat: use RAK hardware crypto on advert processing While working on some sensor code implementations, I ran into some hard crashes that root-caused to the 4KB loop task stack being exhausted. Looking for various optimization schemes resulted in this relatively low-lift fix. RAK3401 and RAK4631 both support hardware crypto. Rather than loading in one of the two software crypto libs, we can just use the onboard hardware. This is faster, should consume less power, and in testing used a scant up to 700 bytes in the run loop vs 2.5-3KB per advert. This change **only** affects advert verification processing, which currently consumes a significant chunk of the 4KB run loop. I figured such a change should likely be implemented in phases. After soaking, this hardware crypto verification process could be implemented across the entire MeshCore cryptographic function on RAK nodes. Also possible other nodes have available hardware crypto, however, I have not checked, so future improvements may also exist there. Tested on: - RAK3401 RAK 1W - RAK4631 19001 --- src/Identity.cpp | 20 +++++++++++++++++++- variants/rak3401/platformio.ini | 1 + variants/rak4631/platformio.ini | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index ea546274d..2ab1cefaa 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -4,6 +4,11 @@ #include #include +#ifdef USE_CC310_ED25519 +#include +#include "nrf_cc310/include/crys_ec_edw_api.h" +#endif + namespace mesh { Identity::Identity() { @@ -15,7 +20,20 @@ Identity::Identity(const char* pub_hex) { } bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const { -#if 0 +#ifdef USE_CC310_ED25519 + // nRF52840 CryptoCell CC310 hardware Ed25519 verification. The software + // implementations need ~3KB of stack (which can overflow the Adafruit core's + // 4KB loop task stack from the advert receive path); the hardware path + // needs much less, around 600-700bytes. The CC310 workspace is static, faster, + // should save power at scale as well. + static CRYS_ECEDW_TempBuff_t cc310_tmp; + nRFCrypto.begin(); + CRYSError_t rc = CRYS_ECEDW_Verify((uint8_t*)sig, CRYS_ECEDW_SIGNATURE_BYTES, + (uint8_t*)pub_key, CRYS_ECEDW_MOD_SIZE_IN_BYTES, + (uint8_t*)message, (size_t)msg_len, &cc310_tmp); + nRFCrypto.end(); + return rc == CRYS_OK; +#elif 0 // NOTE: memory corruption bug was found in this function!! return ed25519_verify(sig, message, msg_len, pub_key); #else diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b..9537fc7f2 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -13,6 +13,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM + -D USE_CC310_ED25519=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 2bbba3146..f6b634ade 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -22,6 +22,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D USE_CC310_ED25519=1 -D ENV_INCLUDE_RAK12035=1 -UENV_INCLUDE_BME680 -D ENV_INCLUDE_BME680_BSEC=1 From 7a2764ebd64d74812e91d351b6008631368b1376 Mon Sep 17 00:00:00 2001 From: Nick Dunklee Date: Wed, 24 Jun 2026 15:58:09 -0600 Subject: [PATCH 054/191] Added Heltec t096 and seeed t1000-e Tested Heltec t096 and Seeed t1000-e, both support this hardware feature. --- variants/heltec_t096/platformio.ini | 1 + variants/t1000-e/platformio.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index e820bf58d..4da11c0ce 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -29,6 +29,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D USE_CC310_ED25519=1 -D PIN_VEXT_EN=26 ; Vext is connected to VDD which is also connected to TFT & GPS -D PIN_VEXT_EN_ACTIVE=HIGH -D PIN_GPS_RX=25 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 43a3d93f6..9218c5787 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D RF_SWITCH_TABLE -D RX_BOOSTED_GAIN=true + -D USE_CC310_ED25519=1 -D P_LORA_BUSY=7 ; P0.7 -D P_LORA_SCLK=11 ; P0.11 -D P_LORA_NSS=12 ; P0.12 From b975180fc88e441041ec52ffa49b36b0a215e1b0 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 25 Jun 2026 16:03:27 +1200 Subject: [PATCH 055/191] 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 000000000..2a0c5e583 --- /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 000000000..6b1bf8602 --- /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 000000000..f591b5acc --- /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 000000000..845a8db1d --- /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 000000000..ea421b07d --- /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 000000000..f4b898d8e --- /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 000000000..34213f57c --- /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 000000000..e056ab647 --- /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 000000000..8b1378917 --- /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 056/191] 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 5cc3a9a11..5f9284528 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 24c4b1f2a..e66f7e596 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -252,7 +252,6 @@ public: // 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 057/191] 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 000000000..4e40ff096 --- /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 000000000..73ac0a848 --- /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 000000000..ea558f63c --- /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 058/191] 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 4e40ff096..148019208 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 059/191] 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 148019208..83495cd83 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 73ac0a848..d53b6a1c3 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 ea558f63c..87c55f0f2 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 060/191] 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 20f7dc243..6a2f3dec2 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 061/191] 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 5f9284528..ca4cfad27 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 e66f7e596..fb091a4cf 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -252,6 +252,6 @@ public: // 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 10cb00c77..f3abcf477 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -111,8 +111,8 @@ public: // 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 67b1448e8..f474215a1 100644 --- a/src/helpers/esp32/ESPNOWRadio.h +++ b/src/helpers/esp32/ESPNOWRadio.h @@ -38,7 +38,7 @@ public: * 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 8861f76d2..851fd644b 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -33,8 +33,8 @@ public: 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 13efd25b5..fc5052839 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -36,8 +36,8 @@ public: 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 cc7bb2238..1d103f579 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -40,8 +40,8 @@ public: 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 9ddea78f3..bce56b996 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -37,8 +37,8 @@ public: 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 9943bcab7..3091832f1 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -66,7 +66,7 @@ public: 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 062/191] 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 dce1c5d89..f82d40e25 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 063/191] 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 f82d40e25..c95e3e34b 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 064/191] * 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 fc5052839..c6b1acb47 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -26,6 +26,11 @@ public: 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 065/191] 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 857525d46..f35f3b2d9 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 066/191] 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 f35f3b2d9..2ccf950f4 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 5c534c4399a01bcef6681eaa7fb62fafb6da8fcf Mon Sep 17 00:00:00 2001 From: Jody Bentley Date: Mon, 29 Jun 2026 14:49:11 -0400 Subject: [PATCH 067/191] thinknode_m6: don't drive GPS REINIT pin (fixes GPS never detected) The M6's L76K GPS streams NMEA at 9600 baud on its own, but the firmware reported no GPS. Root cause: pin 29 (PIN_GPS_RESET / the module's REINIT line) was driven HIGH, which holds the L76K silent so it never emits any sentences and detection fails. variant.cpp drove pin 29 HIGH at boot, and because PIN_GPS_RESET was defined, MicroNMEALocationProvider also drove it HIGH in begin(). Bench testing on a sealed M6 (passive NMEA capture, no logic analyzer) confirmed: pin 29 driven HIGH = 0 bytes; pin 29 floating = full NMEA stream. Define GPS_RESET (-1) so the location provider never touches pin 29, and stop driving it in initVariant. This matches the Meshtastic M6 variant, which leaves the same pin as a floating input. --- variants/thinknode_m6/variant.cpp | 6 ++++-- variants/thinknode_m6/variant.h | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/variants/thinknode_m6/variant.cpp b/variants/thinknode_m6/variant.cpp index c88f387db..79930e2d0 100644 --- a/variants/thinknode_m6/variant.cpp +++ b/variants/thinknode_m6/variant.cpp @@ -30,6 +30,8 @@ void initVariant() { digitalWrite(PIN_GPS_STANDBY, HIGH); pinMode(PIN_GPS_EN, OUTPUT); digitalWrite(PIN_GPS_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, HIGH); + // PIN_GPS_RESET (pin 29 / REINIT) is intentionally left floating (input). + // Driving it HIGH holds the L76K silent, so it never streams NMEA and the + // firmware reports "no GPS". Letting it float lets the module run, matching + // the Meshtastic M6 variant. See GPS_RESET (-1) in variant.h. } diff --git a/variants/thinknode_m6/variant.h b/variants/thinknode_m6/variant.h index 70fd65062..0a4025f3b 100644 --- a/variants/thinknode_m6/variant.h +++ b/variants/thinknode_m6/variant.h @@ -102,7 +102,12 @@ #define PIN_GPS_RX (2) #define PIN_GPS_TX (3) #define PIN_GPS_EN (6) // EN -#define PIN_GPS_RESET (29) +#define PIN_GPS_RESET (29) // REINIT - must FLOAT; driving it (esp. HIGH) silences the L76K +// The M6's L76K streams NMEA on its own and must not have its REINIT pin driven. +// Tell the location provider there is no reset pin so it never touches pin 29 +// (driving it HIGH holds the module silent). Matches Meshtastic, which leaves +// this pin as an input. See variant.cpp (pin 29 is intentionally not configured). +#define GPS_RESET (-1) #define PIN_GPS_STANDBY (30) // STANDBY #define PIN_GPS_PPS (31) #define GPS_BAUD_RATE 9600 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 068/191] 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 ad2012290..ca62fc26a 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 069/191] 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 4f9e1c59b..13d410de3 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 e16f7b830..b08854e5d 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 070/191] 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 b08854e5d..47cc0ab8c 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 e9b92262c..c11f37cac 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 932541db5..49a299a6a 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -15,8 +15,9 @@ public: */ 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 0b79cfb42..956f36faa 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -31,27 +31,31 @@ public: } #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 d2e2e5e00..8093d3cb5 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 04c1564bd..8bbe64667 100644 --- a/src/helpers/bridges/BridgeBase.h +++ b/src/helpers/bridges/BridgeBase.h @@ -110,7 +110,7 @@ protected: * @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 808e9df43..3c094e7c9 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 0024f6f2f..f719d342e 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 b6e551a07..0cefe5c6e 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 000000000..46b477d94 --- /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 071/191] 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 ca4cfad27..b66e19522 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 fb091a4cf..0b2e7491b 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -166,7 +166,7 @@ protected: } #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 12d0b0c31..36978e808 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 e9e53ec91..6bab9dc2d 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -154,7 +154,7 @@ protected: 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 3af5ffe11923eff8da35a14baf505f207fc33d2d Mon Sep 17 00:00:00 2001 From: Naomi Smith Date: Fri, 3 Jul 2026 11:55:06 -0700 Subject: [PATCH 072/191] Fix GPS and LEDs on ThinkNode M3 GPS support was implemented incorrectly for the ThinkNode M3. The reset pin was being driven when it should be left floating for this unit. The enable pin also wasn't being picked up by `MicroNMEALocationProvider` because of a mismatch in constant naming conventions. I did a general cleanup of the GPS and ThinkNode M3 bring-up code so that constant names line up and "*_ACTIVE" constants are used consistently vs hardcoding `HIGH`/`LOW`. After making these changes, serial data immediately starts streaming in from the NMEA on boot and GPS detection just works. LED handling was also not quite right for the ThinkNode M3. The LoRa TX LED was being driven high to turn it on when it should actually be driven low. I changed the code to use the `LED_STATE_ON` constant and also added a little code to the shutdown path to properly make sure that all LEDs are turned off. Tested and confirmed working on real hardware. Resolves both #1864 and #2879. --- .../sensors/MicroNMEALocationProvider.h | 32 +++++++++++-------- variants/thinknode_m3/ThinkNodeM3Board.h | 12 +++---- variants/thinknode_m3/platformio.ini | 2 -- variants/thinknode_m3/variant.cpp | 8 +++-- variants/thinknode_m3/variant.h | 9 +++--- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3a..9d697fed3 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -13,8 +13,12 @@ #endif #endif -#ifndef PIN_GPS_EN_ACTIVE - #define PIN_GPS_EN_ACTIVE HIGH +#ifndef GPS_EN_ACTIVE + #ifdef PIN_GPS_EN_ACTIVE + #define GPS_EN_ACTIVE PIN_GPS_EN_ACTIVE + #else + #define GPS_EN_ACTIVE HIGH + #endif #endif #ifndef GPS_RESET @@ -25,11 +29,11 @@ #endif #endif -#ifndef GPS_RESET_FORCE +#ifndef GPS_RESET_ACTIVE #ifdef PIN_GPS_RESET_ACTIVE - #define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE + #define GPS_RESET_ACTIVE PIN_GPS_RESET_ACTIVE #else - #define GPS_RESET_FORCE LOW + #define GPS_RESET_ACTIVE LOW #endif #endif @@ -52,11 +56,11 @@ public : _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) { if (_pin_reset != -1) { pinMode(_pin_reset, OUTPUT); - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } if (_pin_en != -1) { pinMode(_pin_en, OUTPUT); - digitalWrite(_pin_en, LOW); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } } @@ -76,27 +80,27 @@ public : void begin() override { claim(); if (_pin_en != -1) { - digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void reset() override { if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); delay(10); - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void stop() override { if (_pin_en != -1) { - digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } release(); } @@ -105,7 +109,7 @@ public : // directly read the enable pin if present as gps can be // activated/deactivated outside of here ... if (_pin_en != -1) { - return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE; + return digitalRead(_pin_en) == GPS_EN_ACTIVE; } else { return true; // no enable so must be active } diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 1435d31d4..63b141161 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -18,12 +18,12 @@ public: void begin(); uint16_t getBattMilliVolts() override; -#if defined(P_LORA_TX_LED) +#ifdef P_LORA_TX_LED void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off } #endif @@ -44,9 +44,9 @@ public: void powerOff() override { // turn off all leds, sd_power_system_off will not do this for us - #ifdef P_LORA_TX_LED - digitalWrite(P_LORA_TX_LED, LOW); - #endif + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); // power off board sd_power_system_off(); diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0a3d4eda9..0004f4c73 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -24,7 +24,6 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_MOSI=46 -D P_LORA_RESET=42 -D P_LORA_TX_LED=PIN_LED_BLUE - -D P_LORA_TX_LED_ON=LOW -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 -D MESH_DEBUG=1 @@ -105,7 +104,6 @@ build_flags = ${ThinkNode_M3.build_flags} -D BLE_TX_POWER=0 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D GPS_NMEA_DEBUG -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=23 diff --git a/variants/thinknode_m3/variant.cpp b/variants/thinknode_m3/variant.cpp index dad0f3f55..b47b83543 100644 --- a/variants/thinknode_m3/variant.cpp +++ b/variants/thinknode_m3/variant.cpp @@ -80,16 +80,18 @@ void initVariant() digitalWrite(LED_POWER, HIGH); pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); pinMode(PIN_LED_GREEN, OUTPUT); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); pinMode(PIN_LED_RED, OUTPUT); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(PIN_GPS_POWER, OUTPUT); pinMode(PIN_GPS_EN, OUTPUT); - pinMode(PIN_GPS_RESET, OUTPUT); // Power on gps but in standby - digitalWrite(PIN_GPS_EN, LOW); - digitalWrite(PIN_GPS_POWER, HIGH); + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); } diff --git a/variants/thinknode_m3/variant.h b/variants/thinknode_m3/variant.h index 02ed78a84..78dfab85d 100644 --- a/variants/thinknode_m3/variant.h +++ b/variants/thinknode_m3/variant.h @@ -32,7 +32,7 @@ #define EXT_CHRG_DETECT (32) // P1.3 #define EXT_PWR_DETECT (31) // P0.5 -#define PIN_VBAT_READ (5) +#define PIN_VBAT_READ (5) #define AREF_VOLTAGE (2.4f) #define ADC_MULTIPLIER (2.0) //(1.75f) // 2.0 gives more coherent value, 4.2V when charged, needs tweaking @@ -92,18 +92,19 @@ // GPS #define HAS_GPS 1 -#define PIN_GPS_RX (22) +#define PIN_GPS_RX (22) #define PIN_GPS_TX (20) #define PIN_GPS_POWER (14) #define PIN_GPS_EN (21) // STANDBY #define PIN_GPS_RESET (25) // REINIT -#define GPS_RESET_ACTIVE LOW +#define GPS_POWER_ACTIVE HIGH #define GPS_EN_ACTIVE HIGH +#define GPS_RESET (-1) #define GPS_BAUDRATE 9600 //////////////////////////////////////////////////////////////////////////////// // Buzzer #define BUZZER_EN (37) // P1.5 -#define BUZZER_PIN (25) // P0.25 \ No newline at end of file +#define BUZZER_PIN (25) // P0.25 From b40968a0f08b93cf14104e6f8d7ef45338c913ab Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 5 Jul 2026 12:23:22 +1200 Subject: [PATCH 073/191] 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 8057bc70a..000000000 --- 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 074/191] 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 e0ca1d0ee..a55abb264 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 1efc99f36..45d7761b6 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -62,6 +62,9 @@ public: 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 075/191] 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 17265f045..beee32126 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 17065cf44..cbf4cd498 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ public: 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 076/191] 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 7c8420194..403ea463f 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 552904677..b48f64121 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 0119475e7..452c02d41 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 077/191] 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 ab7ecc245..0b0f701c2 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h @@ -47,7 +47,7 @@ public: 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 33c3d05fc..66c770994 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h @@ -47,7 +47,7 @@ public: 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 aa1772e4c..39b55911a 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h @@ -47,7 +47,7 @@ public: 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 da792b78b..805ca67f2 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.h @@ -38,7 +38,7 @@ public: 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 54425145c..78af529d6 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 490f86787..1ef5b766b 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 f27dc291d..bd76d3de4 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -50,10 +50,7 @@ public: #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 752b27e75..25d4d8676 100644 --- a/variants/keepteen_lt1/KeepteenLT1Board.h +++ b/variants/keepteen_lt1/KeepteenLT1Board.h @@ -37,8 +37,4 @@ public: 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 ea1782cf7..5fafcad86 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -48,12 +48,10 @@ public: } 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 e560cd14b..e957d2e57 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -24,6 +24,7 @@ public: } void powerOff() override { + NRF52Board::powerOff(); #ifdef LED_RED digitalWrite(LED_RED, HIGH); #endif @@ -39,6 +40,5 @@ public: #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 f0dcef312..8143587d5 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 7a43fd838..f4c16016b 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -22,6 +22,8 @@ public: } void powerOff() override { + NRF52Board::powerOff(); + digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED digitalWrite(LED_RED, LOW); @@ -38,6 +40,5 @@ public: #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 478bd56d7..e215eb776 100644 --- a/variants/mesh_pocket/MeshPocket.h +++ b/variants/mesh_pocket/MeshPocket.h @@ -32,8 +32,4 @@ public: 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 b69c0e419..67a521fe9 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -60,7 +60,7 @@ public: 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 4fa5cd41e..450a3d13b 100644 --- a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h @@ -65,7 +65,7 @@ public: #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 cf771efe7..bf9543b03 100644 --- a/variants/nano_g2_ultra/nano-g2.h +++ b/variants/nano_g2_ultra/nano-g2.h @@ -52,6 +52,6 @@ public: 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 7b6afb1b3..b190c47c8 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -72,8 +72,4 @@ public: #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 cc5aa06f5..daa90d746 100644 --- a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h +++ b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h @@ -69,6 +69,6 @@ public: 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 6799a5e92..5a8861c29 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -54,7 +54,7 @@ public: #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 e7653fb2f..1111d3c40 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -88,6 +88,6 @@ public: 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 ebc46e6e6..94224d9c2 100644 --- a/variants/thinknode_m1/ThinkNodeM1Board.h +++ b/variants/thinknode_m1/ThinkNodeM1Board.h @@ -40,7 +40,6 @@ public: #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 1435d31d4..396d80d1a 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -49,6 +49,6 @@ public: #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 32baa2a0a..78815e2c9 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -44,6 +44,6 @@ public: #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 052238e66..e376e0666 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -35,6 +35,6 @@ public: } void powerOff() override { - sd_power_system_off(); + NRF52Board::powerOff(); } }; diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index 2790dbad9..b2638a440 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -50,7 +50,7 @@ public: 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 078/191] 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 45d7761b6..d7eb5fee2 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 079/191] 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 | 58 ++++++++----------- 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, 50 insertions(+), 418 deletions(-) create mode 100644 variants/station_g3_esp32/StationG3Board.cpp diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 65e111029..0325161d5 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 @@ public: } } - 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 af1153182..88737c4d5 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 2192c1410..fadc038f9 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 @@ public: 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 3994a2061..96ec59c9b 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 645ec3481..f287227c6 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 @@ public: 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 4f35be400..0a16b52b8 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 bc38c1e09..557c070ec 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 @@ public: 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 f182c905e..99b1cdfe0 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 ccbecc7ab..2bd6a0254 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 @@ public: 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 fe800890b..9b08fe943 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 @@ public: } } - 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 ba22a7f2b..7e7abe313 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 @@ public: } } - 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 3978c51ea..3f13f41f6 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 fc37b9f6a..55166bb37 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 @@ public: 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 7ed007af9..e2844360d 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 @@ public: } #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 8ba3197cf..704162b8c 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 @@ public: } } - 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 a905682c8..d1989ee0f 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 @@ public: } } - 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 000000000..4a4983110 --- /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 dc440d95f..4b1fb81c1 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -73,36 +73,7 @@ public: 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 059651036..8d68006d2 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 - } - - void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_deep_sleep_start(); - } - - void ThinknodeM2Board::powerOff() { - enterDeepSleep(0); - } - - 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 ); + 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 } - const char* ThinknodeM2Board::getManufacturerName() const { - return "Elecrow ThinkNode M2"; +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); +} + +const char *ThinknodeM2Board::getManufacturerName() const { + return "Elecrow ThinkNode M2"; +} diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 8011fae67..025567778 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 c4de538c5..2cb138e63 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 3c120027b..57ff0d00e 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 6ea1c15ff..c5700c688 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 @@ public: #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 584f9e909be71610ffe3b789f33ca02d10ea11f9 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:52:18 +1000 Subject: [PATCH 080/191] Lilygo T-Echo Lite - Change SX1262 to use LDO instead of DC-DC (DCC pin on SX1262 not wired in, according to schematic) --- variants/lilygo_techo_lite/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 4b5edf691..a9b3d124d 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -15,6 +15,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_USE_REGULATOR_LDO=1 -D P_LORA_TX_LED=LED_GREEN -D DISABLE_DIAGNOSTIC_OUTPUT -D ENV_INCLUDE_GPS=1 From cea246d0eba80af7d7a4447d7ad6f972222f83b7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 6 Jul 2026 15:09:18 +1000 Subject: [PATCH 081/191] * super fast ST7735 display driver --- src/helpers/ui/ST7735Display.cpp | 553 +++++++++++++++++++++++++--- src/helpers/ui/ST7735Display.h | 9 +- variants/heltec_t096/platformio.ini | 2 +- 3 files changed, 502 insertions(+), 62 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index a6087dd81..2b9f3ebbe 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -1,12 +1,404 @@ #include "ST7735Display.h" -#ifndef DISPLAY_ROTATION - #define DISPLAY_ROTATION 2 -#endif +//#include + +// Optimised ST7735 display driver, derived from Adafruit_ST7735 library. + +#define ST_CMD_DELAY 0x80 // special signifier for command lists + +#define ST77XX_NOP 0x00 +#define ST77XX_SWRESET 0x01 +#define ST77XX_RDDID 0x04 +#define ST77XX_RDDST 0x09 + +#define ST77XX_SLPIN 0x10 +#define ST77XX_SLPOUT 0x11 +#define ST77XX_PTLON 0x12 +#define ST77XX_NORON 0x13 + +#define ST77XX_INVOFF 0x20 +#define ST77XX_INVON 0x21 +#define ST77XX_DISPOFF 0x28 +#define ST77XX_DISPON 0x29 +#define ST77XX_CASET 0x2A +#define ST77XX_RASET 0x2B +#define ST77XX_RAMWR 0x2C +#define ST77XX_RAMRD 0x2E + +#define ST77XX_PTLAR 0x30 +#define ST77XX_TEOFF 0x34 +#define ST77XX_TEON 0x35 +#define ST77XX_MADCTL 0x36 +#define ST77XX_COLMOD 0x3A + +#define ST77XX_MADCTL_MY 0x80 +#define ST77XX_MADCTL_MX 0x40 +#define ST77XX_MADCTL_MV 0x20 +#define ST77XX_MADCTL_ML 0x10 +#define ST77XX_MADCTL_RGB 0x00 + +#define ST77XX_RDID1 0xDA +#define ST77XX_RDID2 0xDB +#define ST77XX_RDID3 0xDC +#define ST77XX_RDID4 0xDD + +// Some ready-made 16-bit ('565') color settings: +#define ST77XX_BLACK 0x0000 +#define ST77XX_WHITE 0xFFFF +#define ST77XX_RED 0xF800 +#define ST77XX_GREEN 0x07E0 +#define ST77XX_BLUE 0x001F +#define ST77XX_CYAN 0x07FF +#define ST77XX_MAGENTA 0xF81F +#define ST77XX_YELLOW 0xFFE0 +#define ST77XX_ORANGE 0xFC00 + + +// some flags for initR() :( +#define INITR_GREENTAB 0x00 +#define INITR_REDTAB 0x01 +#define INITR_BLACKTAB 0x02 +#define INITR_18GREENTAB INITR_GREENTAB +#define INITR_18REDTAB INITR_REDTAB +#define INITR_18BLACKTAB INITR_BLACKTAB +#define INITR_144GREENTAB 0x01 +#define INITR_MINI160x80 0x04 +#define INITR_HALLOWING 0x05 +#define INITR_MINI160x80_PLUGIN 0x06 + +// Some register settings +#define ST7735_MADCTL_BGR 0x08 +#define ST7735_MADCTL_MH 0x04 + +#define ST7735_FRMCTR1 0xB1 +#define ST7735_FRMCTR2 0xB2 +#define ST7735_FRMCTR3 0xB3 +#define ST7735_INVCTR 0xB4 +#define ST7735_DISSET5 0xB6 + +#define ST7735_PWCTR1 0xC0 +#define ST7735_PWCTR2 0xC1 +#define ST7735_PWCTR3 0xC2 +#define ST7735_PWCTR4 0xC3 +#define ST7735_PWCTR5 0xC4 +#define ST7735_VMCTR1 0xC5 + +#define ST7735_PWCTR6 0xFC + +#define ST7735_GMCTRP1 0xE0 +#define ST7735_GMCTRN1 0xE1 + +// Some ready-made 16-bit ('565') color settings: +#define ST7735_BLACK ST77XX_BLACK +#define ST7735_WHITE ST77XX_WHITE +#define ST7735_RED ST77XX_RED +#define ST7735_GREEN ST77XX_GREEN +#define ST7735_BLUE ST77XX_BLUE +#define ST7735_CYAN ST77XX_CYAN +#define ST7735_MAGENTA ST77XX_MAGENTA +#define ST7735_YELLOW ST77XX_YELLOW +#define ST7735_ORANGE ST77XX_ORANGE + +static TFT_eSPI lcd = TFT_eSPI(160, 80); +static uint32_t curr_color; + +#define _spi (&SPI1) + +SPISettings _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); + +// clang-format off +static const uint8_t PROGMEM + Bcmd[] = { // Init commands for 7735B screens + 18, // 18 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + 50, // 50 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + 255, // 255 = max (500 ms) delay + ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + 0x05, // 16-bit color + 10, // 10 ms delay + ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: + 0x00, // fastest refresh + 0x06, // 6 lines front porch + 0x03, // 3 lines back porch + 10, // 10 ms delay + ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: + 0x08, // Row/col addr, bottom-top refresh + ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: + 0x15, // 1 clk cycle nonoverlap, 2 cycle gate + // rise, 3 cycle osc equalize + 0x02, // Fix on VTL + ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: + 0x0, // Line inversion + ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: + 0x02, // GVDD = 4.7V + 0x70, // 1.0uA + 10, // 10 ms delay + ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: + 0x05, // VGH = 14.7V, VGL = -7.35V + ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: + 0x01, // Opamp current small + 0x02, // Boost frequency + ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: + 0x3C, // VCOMH = 4V + 0x38, // VCOML = -1.1V + 10, // 10 ms delay + ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: + 0x11, 0x15, + ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides + 0x21, 0x1B, 0x13, 0x19, // accurate colors) + 0x17, 0x15, 0x1E, 0x2B, + 0x04, 0x05, 0x02, 0x0E, + ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides + 0x22, 0x1D, 0x18, 0x1E, // accurate colors) + 0x1B, 0x1A, 0x24, 0x2B, + 0x06, 0x06, 0x02, 0x0F, + 10, // 10 ms delay + ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 2 + 0x00, 0x81, // XEND = 129 + ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 1 + 0x00, 0x81, // XEND = 160 + ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay + 255 }, // 255 = max (500 ms) delay + + Rcmd1[] = { // 7735R init, part 1 (red or green tab) + 15, // 15 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, // 150 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay + 255, // 500 ms delay + ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + 0x07, // No inversion + ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + 0x0E, + ST77XX_INVOFF, 0, // 13: Don't invert display, no args + ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + 0xC8, // row/col addr, bottom-top refresh + ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + 0x05 }, // 16-bit color + + Rcmd2green[] = { // 7735R init, part 2 (green tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 0 + 0x00, 0x7F+0x02, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x01, // XSTART = 0 + 0x00, 0x9F+0x01 }, // XEND = 159 + + Rcmd2red[] = { // 7735R init, part 2 (red tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd2green144[] = { // 7735R init, part 2 (green 1.44 tab) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F }, // XEND = 127 + + Rcmd2green160x80[] = { // 7735R init, part 2 (mini 160x80) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd2green160x80plugin[] = { // 7735R init, part 2 (mini 160x80 with plugin FPC) + 3, // 3 commands in list: + ST77XX_INVON, 0, // 1: Display is inverted + ST77XX_CASET, 4, // 2: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 3: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd3[] = { // 7735R init, part 3 (red or green tab) + 4, // 4 commands in list: + ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay + 100 }; // 100 ms delay + +static int16_t _xstart = 0; ///< Internal framebuffer X offset +static int16_t _ystart = 0; ///< Internal framebuffer Y offset +static uint8_t _colstart = 0; ///< Some displays need this changed to offset +static uint8_t _rowstart = 0; ///< Some displays need this changed to offset +static uint8_t rotation = 0; +static int16_t _width = 0; ///< Display width as modified by current rotation +static int16_t _height = 0; ///< Display height as modified by current rotation + +static void set_CS(uint8_t level) { + //if (_cs != (uint8_t) -1) { + digitalWrite(PIN_TFT_CS, level); + //} +} +static void sendCommand(uint8_t com) { + set_CS(HIGH); + digitalWrite(PIN_TFT_DC, LOW); + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(com); + _spi->endTransaction(); + set_CS(HIGH); + digitalWrite(PIN_TFT_DC, HIGH); +} + +static void WriteData(uint8_t data) { + digitalWrite(PIN_TFT_CS, LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(data); + _spi->endTransaction(); + digitalWrite(PIN_TFT_CS, HIGH); +} +static void SPI_WRITE32(uint32_t l) { + _spi->transfer(l >> 24); + _spi->transfer(l >> 16); + _spi->transfer(l >> 8); + _spi->transfer(l); +} +static void writeCommand(uint8_t cmd) { + digitalWrite(PIN_TFT_DC, LOW); + _spi->transfer(cmd); + digitalWrite(PIN_TFT_DC, HIGH); +} + +static void displayInit(const uint8_t *addr) { + uint8_t numCommands, cmd, numArgs; + uint16_t ms; + + numCommands = pgm_read_byte(addr++); // Number of commands to follow + while (numCommands--) { // For each command... + cmd = pgm_read_byte(addr++); // Read command + numArgs = pgm_read_byte(addr++); // Number of args to follow + ms = numArgs & ST_CMD_DELAY; // If hibit set, delay follows args + numArgs &= ~ST_CMD_DELAY; // Mask out delay bit + sendCommand(cmd); + for (int k = 0; k < numArgs; k++) { + WriteData(addr[k]); + } + addr += numArgs; + + if (ms) { + ms = pgm_read_byte(addr++); // Read post-command delay time (ms) + if (ms == 255) + ms = 500; // If 255, delay for 500 ms + delay(ms); + } + } +} + +static void setRotation(uint8_t m) { + uint8_t madctl = 0; + + rotation = m & 3; // can't be higher than 3 + + switch (rotation) { + case 0: + madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY | ST7735_MADCTL_BGR; + + _height = 160; + _width = 80; + _xstart = _colstart; + _ystart = _rowstart; + break; + case 1: + madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR; + + _width = 160; + _height = 80; + _ystart = _colstart; + _xstart = _rowstart; + break; + case 2: + madctl = ST7735_MADCTL_BGR; + + _height = 160; + _width = 80; + _xstart = _colstart; + _ystart = _rowstart; + break; + case 3: + madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR; + + _width = 160; + _height = 80; + _ystart = _colstart; + _xstart = _rowstart; + break; + } + + sendCommand(ST77XX_MADCTL); + WriteData(madctl); +} + +static void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + x += _xstart; + y += _ystart; + uint32_t xa = ((uint32_t)x << 16) | (x + w - 1); + uint32_t ya = ((uint32_t)y << 16) | (y + h - 1); + + writeCommand(ST77XX_CASET); // Column addr set + SPI_WRITE32(xa); + + writeCommand(ST77XX_RASET); // Row addr set + SPI_WRITE32(ya); + + writeCommand(ST77XX_RAMWR); // write to RAM +} #define SCALE_X 1.25f // 160 / 128 #define SCALE_Y 1.25f // 80 / 64 +static TFT_eSprite *sprite = NULL; + bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { return true; /* @@ -16,35 +408,69 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { */ } +#ifndef PIN_TFT_LEDA_CTL_ACTIVE + #define PIN_TFT_LEDA_CTL_ACTIVE HIGH +#endif + bool ST7735Display::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); - pinMode(PIN_TFT_LEDA_CTL, OUTPUT); -#if defined(PIN_TFT_LEDA_CTL_ACTIVE) - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + delay(3000); // TEMP!! + pinMode(PIN_TFT_RST, OUTPUT); + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + + // Pulse Reset low for 10ms + digitalWrite(PIN_TFT_RST, HIGH); + delay(1); + digitalWrite(PIN_TFT_RST, LOW); + delay(10); + digitalWrite(PIN_TFT_RST, HIGH); +#ifdef ESP_PLATFORM + _spi->begin(_clk,_miso,_mosi,-1); #else - digitalWrite(PIN_TFT_LEDA_CTL, HIGH); + _spi->begin(); #endif + _spi->setClockDivider(SPI_CLOCK_DIV2); + + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); digitalWrite(PIN_TFT_RST, HIGH); -#if defined(HELTEC_T1) - display.initR(INITR_MINI160x80); - display.setRotation(DISPLAY_ROTATION); -#elif defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) - display.initR(INITR_MINI160x80); - display.setRotation(DISPLAY_ROTATION); - uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR - display.sendCommand(ST77XX_MADCTL, &madctl, 1); -#else - display.initR(INITR_MINI160x80_PLUGIN); - display.setRotation(DISPLAY_ROTATION); + displayInit(Rcmd1); + + _height = 80; + _width = 160; + _colstart = 24; + _rowstart = 0; + +#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) + displayInit(Rcmd2green160x80); + //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR + //display.sendCommand(ST77XX_MADCTL, &madctl, 1); #endif - display.setSPISpeed(40000000); - display.fillScreen(ST77XX_BLACK); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(2); - display.cp437(true); // Use full 256 char 'Code Page 437' font + + displayInit(Rcmd3); + + setRotation(DISPLAY_ROTATION); + + sendCommand(ST77XX_DISPON); + + if (!sprite) { + // alloc offscreen canvas + sprite = new TFT_eSprite(&lcd); + if (sprite) { + if (sprite->createSprite(160, 80)) { + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + } else { + Serial.printf("ST7735Display: failed to alloc canvas pixels"); + } + } else { + Serial.printf("ST7735Display: failed to alloc canvas"); + } + } _isOn = true; } @@ -52,17 +478,22 @@ bool ST7735Display::begin() { } void ST7735Display::turnOn() { - ST7735Display::begin(); + if (!_isOn) { + sendCommand(ST77XX_DISPON); + + // Now turn on the backlight + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + _isOn = true; + } } void ST7735Display::turnOff() { if (_isOn) { - digitalWrite(PIN_TFT_RST, LOW); -#if defined(PIN_TFT_LEDA_CTL_ACTIVE) + sendCommand(ST77XX_DISPOFF); + + //digitalWrite(PIN_TFT_RST, LOW); + // Now turn off the backlight digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE); -#else - digitalWrite(PIN_TFT_LEDA_CTL, LOW); -#endif _isOn = false; if (_peripher_power) _peripher_power->release(); @@ -70,78 +501,90 @@ void ST7735Display::turnOff() { } void ST7735Display::clear() { - //Serial.println("DBG: display.Clear"); - display.fillScreen(ST77XX_BLACK); + sprite->fillScreen(ST77XX_BLACK); } void ST7735Display::startFrame(Color bkg) { - display.fillScreen(0x00); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(1); // This one affects size of Please wait... message - display.cp437(true); // Use full 256 char 'Code Page 437' font + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + //sprite->setFreeFont(&FreeSans7pt7b); + sprite->setTextSize(1); // This one affects size of Please wait... message + //sprite->cp437(true); // Use full 256 char 'Code Page 437' font } void ST7735Display::setTextSize(int sz) { - display.setTextSize(sz); + sprite->setTextSize(sz); } void ST7735Display::setColor(Color c) { switch (c) { case DisplayDriver::DARK : - _color = ST77XX_BLACK; + curr_color = ST77XX_BLACK; break; case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; + curr_color = ST77XX_WHITE; break; case DisplayDriver::RED : - _color = ST77XX_RED; + curr_color = ST77XX_RED; break; case DisplayDriver::GREEN : - _color = ST77XX_GREEN; + curr_color = ST77XX_GREEN; break; case DisplayDriver::BLUE : - _color = ST77XX_BLUE; + curr_color = ST77XX_BLUE; break; case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; + curr_color = ST77XX_YELLOW; break; case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; + curr_color = ST77XX_ORANGE; break; default: - _color = ST77XX_WHITE; + curr_color = ST77XX_WHITE; break; } - display.setTextColor(_color); + sprite->setTextColor(curr_color); } void ST7735Display::setCursor(int x, int y) { - display.setCursor(x*SCALE_X, y*SCALE_Y); + sprite->setCursor(x*SCALE_X, y*SCALE_Y); } void ST7735Display::print(const char* str) { - display.print(str); + sprite->print(str); } void ST7735Display::fillRect(int x, int y, int w, int h) { - display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); + sprite->fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color); } void ST7735Display::drawRect(int x, int y, int w, int h) { - display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); + sprite->drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color); } void ST7735Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { - display.drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, _color); + sprite->drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, curr_color); } uint16_t ST7735Display::getTextWidth(const char* str) { - int16_t x1, y1; - uint16_t w, h; - display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); - return w / SCALE_X; + return sprite->textWidth(str) / SCALE_X; } void ST7735Display::endFrame() { - // display.display(); + // blit the canvas buffer to LCD + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + uint16_t x, y; + uint16_t* pixels = (uint16_t *) ((TFT_eSprite *) sprite)->getPointer(); + for (y = 0; y < 80; y++, pixels += 160) { + setAddrWindow(0, y, 160, 1); +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixels, NULL, 2 * 160); +#else + _spi->transfer(pixels, NULL, 2 * 160); +#endif + } + + _spi->endTransaction(); + set_CS(HIGH); } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 94797503f..08778efd0 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -3,28 +3,25 @@ #include "DisplayDriver.h" #include #include -#include -#include +#include "TFT_eSPI.h" #include class ST7735Display : public DisplayDriver { - Adafruit_ST7735 display; bool _isOn; - uint16_t _color; RefCountedDigitalPin* _peripher_power; bool i2c_probe(TwoWire& wire, uint8_t addr); public: #ifdef USE_PIN_TFT ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST), + // display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST), _peripher_power(peripher_power) { _isOn = false; } #else ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), + // display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), _peripher_power(peripher_power) { _isOn = false; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index e820bf58d..0a062f00b 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -51,7 +51,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 debug_tool = jlink upload_protocol = nrfutil From 4fafcd0e23e95d44d2b4ba3f3d7282b1439fd51e Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:21:09 +1000 Subject: [PATCH 082/191] Lilygo T-Echo Lite - Enable nRF52840 DC-DC - board idle usage reduced from ~12mA to ~7mA --- variants/lilygo_techo_lite/TechoBoard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_techo_lite/TechoBoard.cpp b/variants/lilygo_techo_lite/TechoBoard.cpp index a11d31b27..e0b69f985 100644 --- a/variants/lilygo_techo_lite/TechoBoard.cpp +++ b/variants/lilygo_techo_lite/TechoBoard.cpp @@ -6,7 +6,7 @@ #ifdef LILYGO_TECHO void TechoBoard::begin() { - NRF52Board::begin(); + NRF52BoardDCDC::begin(); // Configure battery measurement control BEFORE Wire.begin() // to ensure P0.02 is not claimed by another peripheral From 2efb3af69b9207b23d57b630bd0b1380c4f4405c Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 2 Jul 2026 21:55:19 -0400 Subject: [PATCH 083/191] repeater: poweroff on long press --- .vscode/extensions.json | 9 ---- examples/simple_repeater/UITask.cpp | 79 +++++++++++++++++++---------- examples/simple_repeater/UITask.h | 5 +- examples/simple_repeater/main.cpp | 5 +- 4 files changed, 60 insertions(+), 38 deletions(-) delete mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 8057bc70a..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "pioarduino.pioarduino-ide", - "platformio.platformio-ide" - ], - "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" - ] -} diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 05d863fc4..17c708e31 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -1,4 +1,5 @@ #include "UITask.h" +#include "target.h" #include #include @@ -9,6 +10,8 @@ #define AUTO_OFF_MILLIS 20000 // 20 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds +#define POWEROFF_DELAY 3000 + // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, @@ -29,9 +32,14 @@ static const uint8_t meshcore_logo [] PROGMEM = { void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; + _started_at = millis(); _node_prefs = node_prefs; _display->turnOn(); +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + user_btn.begin(); +#endif + // strip off dash and commit hash by changing dash to null terminator // e.g: v1.2.3-abcdef -> v1.2.3 char *version = strdup(firmware_version); @@ -47,7 +55,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi void UITask::renderCurrScreen() { char tmp[80]; - if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo _display->setColor(DisplayDriver::BLUE); int logoWidth = 128; @@ -57,24 +65,34 @@ void UITask::renderCurrScreen() { const char* website = "https://meshcore.io"; _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t websiteWidth = _display->getTextWidth(website); - _display->setCursor((_display->width() - websiteWidth) / 2, 22); - _display->print(website); + _display->drawTextCentered(_display->width() / 2, 22, website); // version info _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t versionWidth = _display->getTextWidth(_version_info); - _display->setCursor((_display->width() - versionWidth) / 2, 35); - _display->print(_version_info); + _display->drawTextCentered(_display->width() / 2, 35, _version_info); // node type const char* node_type = "< Repeater >"; - uint16_t typeWidth = _display->getTextWidth(node_type); - _display->setCursor((_display->width() - typeWidth) / 2, 48); - _display->print(node_type); - } else { // home screen - // node name + _display->drawTextCentered(_display->width() / 2, 48, node_type); + } else if (_powering_off_at > 0) { + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + + // meshcore website + const char* website = "https://meshcore.io"; + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + _display->drawTextCentered(_display->width()/ 2, 22, website); + + // Powering off + const char* poweroff_string = "Turning OFF"; + uint16_t poffWidth = _display->getTextWidth(poweroff_string); + _display->setCursor((_display->width() - poffWidth) / 2, 48); + _display->drawTextCentered(_display->width()/2, 48, poweroff_string); + } else { _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(DisplayDriver::GREEN); @@ -94,21 +112,19 @@ void UITask::renderCurrScreen() { } void UITask::loop() { -#ifdef PIN_USER_BTN - if (millis() >= _next_read) { - int btnState = digitalRead(PIN_USER_BTN); - if (btnState != _prevBtnState) { - if (btnState == USER_BTN_PRESSED) { // pressed? - if (_display->isOn()) { - // TODO: any action ? - } else { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer - } - _prevBtnState = btnState; +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + int ev = user_btn.check(); + if (ev == BUTTON_EVENT_CLICK) { + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); } - _next_read = millis() + 200; // 5 reads per second + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _display->turnOn(); + Serial.println("Powering Off"); + _powering_off_at = millis() + POWEROFF_DELAY; } #endif @@ -124,4 +140,15 @@ void UITask::loop() { _display->turnOff(); } } + + if (_powering_off_at > 0) { // power off timer armed +#ifdef LED_PIN + digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff +#endif + if (millis() > _powering_off_at) { + _display->turnOff(); + radio_driver.powerOff(); + _board->powerOff(); // should not return + } + } } diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f11..d8e3ce1d2 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -4,15 +4,18 @@ #include class UITask { + mesh::MainBoard* _board; DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; char _version_info[32]; + unsigned long _powering_off_at = 0; + unsigned long _started_at = 0; void renderCurrScreen(); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; } void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 2ce056f52..c13cd99d5 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -5,7 +5,7 @@ #ifdef DISPLAY_CLASS #include "UITask.h" - static UITask ui_task(display); + static UITask ui_task(board, display); #endif StdRNG fast_rng; @@ -130,7 +130,8 @@ void loop() { command[0] = 0; // reset command buffer } -#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) +#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS) + // // Hold the user button to power off the SenseCAP Solar repeater. int btnState = digitalRead(PIN_USER_BTN); if (btnState == LOW) { From 3ee2f778771a14d1606eb711b8030033cfca4a6f Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 6 Jul 2026 08:46:57 -0400 Subject: [PATCH 084/191] 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 403ea463f..28591cc1a 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 b48f64121..34a7342bb 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 452c02d41..a6cbe9de0 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 085/191] 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 b3ee6d177..7839fa973 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 2c9ab2fe5e817a48bd4db6efa8b6ad43c631ddfe Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 7 Jul 2026 13:54:14 +1000 Subject: [PATCH 086/191] * timing fixes (courtesy of Taco) --- src/helpers/ui/ST7735Display.cpp | 107 ++++++++++++++++--------------- src/helpers/ui/ST7735Display.h | 3 + 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 2b9f3ebbe..aad12d1b4 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -168,11 +168,11 @@ static const uint8_t PROGMEM 255 }, // 255 = max (500 ms) delay Rcmd1[] = { // 7735R init, part 1 (red or green tab) - 15, // 15 commands in list: - ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay - 150, // 150 ms delay + 14, // 14 commands in list: + /*ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, */ // 150 ms delay ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay - 255, // 500 ms delay + 120, // 120 ms delay ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: @@ -251,7 +251,7 @@ static const uint8_t PROGMEM 0x00, 0x9F }, // XEND = 159 Rcmd3[] = { // 7735R init, part 3 (red or green tab) - 4, // 4 commands in list: + 2, // 2 commands in list: ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides 0x37, 0x32, 0x29, 0x2d, // accurate colors) @@ -261,11 +261,7 @@ static const uint8_t PROGMEM 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) 0x2E, 0x2E, 0x37, 0x3F, - 0x00, 0x00, 0x02, 0x10, - ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay - 10, // 10 ms delay - ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay - 100 }; // 100 ms delay + 0x00, 0x00, 0x02, 0x10 }; // 100 ms delay static int16_t _xstart = 0; ///< Internal framebuffer X offset static int16_t _ystart = 0; ///< Internal framebuffer Y offset @@ -413,20 +409,30 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #endif bool ST7735Display::begin() { + if (!sprite) { + // alloc offscreen canvas + sprite = new TFT_eSprite(&lcd); + if (sprite) { + if (sprite->createSprite(160, 80)) { + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + } else { + Serial.printf("ST7735Display: failed to alloc canvas pixels"); + } + } else { + Serial.printf("ST7735Display: failed to alloc canvas"); + } + } + if (!_isOn) { if (_peripher_power) _peripher_power->claim(); - delay(3000); // TEMP!! + delay(100); // TEMP!! pinMode(PIN_TFT_RST, OUTPUT); pinMode(PIN_TFT_CS, OUTPUT); pinMode(PIN_TFT_DC, OUTPUT); + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); - // Pulse Reset low for 10ms - digitalWrite(PIN_TFT_RST, HIGH); - delay(1); - digitalWrite(PIN_TFT_RST, LOW); - delay(10); - digitalWrite(PIN_TFT_RST, HIGH); #ifdef ESP_PLATFORM _spi->begin(_clk,_miso,_mosi,-1); #else @@ -434,55 +440,56 @@ bool ST7735Display::begin() { #endif _spi->setClockDivider(SPI_CLOCK_DIV2); - pinMode(PIN_TFT_LEDA_CTL, OUTPUT); - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); - digitalWrite(PIN_TFT_RST, HIGH); - - displayInit(Rcmd1); - _height = 80; _width = 160; _colstart = 24; _rowstart = 0; -#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) - displayInit(Rcmd2green160x80); - //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR - //display.sendCommand(ST77XX_MADCTL, &madctl, 1); -#endif - - displayInit(Rcmd3); - - setRotation(DISPLAY_ROTATION); - + _resetAndInit(); + sendCommand(ST77XX_DISPON); - - if (!sprite) { - // alloc offscreen canvas - sprite = new TFT_eSprite(&lcd); - if (sprite) { - if (sprite->createSprite(160, 80)) { - sprite->fillScreen(ST77XX_BLACK); - sprite->setTextColor(curr_color = ST77XX_WHITE); - } else { - Serial.printf("ST7735Display: failed to alloc canvas pixels"); - } - } else { - Serial.printf("ST7735Display: failed to alloc canvas"); - } - } _isOn = true; } return true; } +void ST7735Display::_resetAndInit() { + // Pulse Reset low for 10ms + digitalWrite(PIN_TFT_RST, HIGH); + delay(2); + digitalWrite(PIN_TFT_RST, LOW); + delay(10); + digitalWrite(PIN_TFT_RST, HIGH); + delay(2); + + // run init commands + displayInit(Rcmd1); +#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) + displayInit(Rcmd2green160x80); + //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR + //display.sendCommand(ST77XX_MADCTL, &madctl, 1); +#endif + displayInit(Rcmd3); + setRotation(DISPLAY_ROTATION); + + // clear the buffer before display on + sprite->fillScreen(ST77XX_BLACK); + endFrame(); + + // turn on backlight + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + +} + void ST7735Display::turnOn() { if (!_isOn) { + if (_peripher_power) _peripher_power->claim(); + _resetAndInit(); sendCommand(ST77XX_DISPON); // Now turn on the backlight - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + // digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); _isOn = true; } } @@ -507,7 +514,7 @@ void ST7735Display::clear() { void ST7735Display::startFrame(Color bkg) { sprite->fillScreen(ST77XX_BLACK); sprite->setTextColor(curr_color = ST77XX_WHITE); - //sprite->setFreeFont(&FreeSans7pt7b); + sprite->setFreeFont(); sprite->setTextSize(1); // This one affects size of Please wait... message //sprite->cp437(true); // Use full 256 char 'Code Page 437' font } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 08778efd0..68c83db25 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -43,4 +43,7 @@ public: void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; void endFrame() override; + +protected: + void _resetAndInit(); }; From 7f8522a3625a33a49599ad022695fe752763a06f Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:12:59 +1000 Subject: [PATCH 087/191] Heltec T114 - nRF52840 DC-DC Regulator - Enable DC-DC regulator on nRF52840 for Heltec T114 board (idle current reduced from ~12mA to ~9mA) --- variants/heltec_t114/T114Board.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index c03d39afb..51db72fc8 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -33,7 +33,7 @@ void T114Board::initiateShutdown(uint8_t reason) { #endif // NRF52_POWER_MANAGEMENT void T114Board::begin() { - NRF52Board::begin(); + NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); From 369016acc952eda75ebbd53fe74380cb617dea67 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 7 Jul 2026 10:20:03 -0400 Subject: [PATCH 088/191] introduce shutdownPeripherals in NRF52Board to prepare for shutdown --- src/helpers/NRF52Board.cpp | 6 +++++- src/helpers/NRF52Board.h | 1 + variants/lilygo_techo/TechoBoard.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index beee32126..088ddc23c 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -298,7 +298,7 @@ float NRF52Board::getMCUTemperature() { return temp * 0.25f; // Convert to *C } -void NRF52Board::powerOff() { +void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS display.turnOff(); @@ -318,6 +318,10 @@ void NRF52Board::powerOff() { // Flush serial buffers Serial.flush(); delay(100); +} + +void NRF52Board::powerOff() { + shutdownPeripherals(); // Enter SYSTEMOFF uint8_t sd_enabled = 0; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index cbf4cd498..dba15f974 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ public: virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual void shutdownPeripherals(); virtual void powerOff() override; virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; diff --git a/variants/lilygo_techo/TechoBoard.h b/variants/lilygo_techo/TechoBoard.h index e957d2e57..867fc24cf 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -23,8 +23,8 @@ public: return "LilyGo T-Echo"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); #ifdef LED_RED digitalWrite(LED_RED, HIGH); #endif From 17d68e328f0eb5d016fe3c6cf4ada6ba64f7aee2 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 8 Jul 2026 16:20:12 +0800 Subject: [PATCH 089/191] Add heltec rc32 board --- boards/heltec-rc32.json | 43 ++ src/helpers/ui/NV3001BDisplay.cpp | 553 +++++++++++++++++++++++ src/helpers/ui/NV3001BDisplay.h | 68 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 66 +++ variants/heltec_rc32/HeltecRC32Board.h | 32 ++ variants/heltec_rc32/pins_arduino.h | 60 +++ variants/heltec_rc32/platformio.ini | 346 ++++++++++++++ variants/heltec_rc32/target.cpp | 45 ++ variants/heltec_rc32/target.h | 31 ++ variants/heltec_rc32/variant.h | 51 +++ 10 files changed, 1295 insertions(+) create mode 100644 boards/heltec-rc32.json create mode 100644 src/helpers/ui/NV3001BDisplay.cpp create mode 100644 src/helpers/ui/NV3001BDisplay.h create mode 100644 variants/heltec_rc32/HeltecRC32Board.cpp create mode 100644 variants/heltec_rc32/HeltecRC32Board.h create mode 100644 variants/heltec_rc32/pins_arduino.h create mode 100644 variants/heltec_rc32/platformio.ini create mode 100644 variants/heltec_rc32/target.cpp create mode 100644 variants/heltec_rc32/target.h create mode 100644 variants/heltec_rc32/variant.h diff --git a/boards/heltec-rc32.json b/boards/heltec-rc32.json new file mode 100644 index 000000000..b9bafa265 --- /dev/null +++ b/boards/heltec-rc32.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_rc32" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/src/helpers/ui/NV3001BDisplay.cpp b/src/helpers/ui/NV3001BDisplay.cpp new file mode 100644 index 000000000..03825cc03 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.cpp @@ -0,0 +1,553 @@ +#include "NV3001BDisplay.h" +#include +#include + +#ifndef SPI_FREQUENCY + #define SPI_FREQUENCY 8000000 +#endif + +#ifndef PIN_TFT_SCL + #error "PIN_TFT_SCL must be defined" +#endif + +#ifndef PIN_TFT_SDA + #error "PIN_TFT_SDA must be defined" +#endif + +#ifndef PIN_TFT_CS + #error "PIN_TFT_CS must be defined" +#endif + +#ifndef PIN_TFT_DC + #error "PIN_TFT_DC must be defined" +#endif + +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + +#ifndef PIN_TFT_RST + #define PIN_TFT_RST -1 +#endif + +#ifndef PIN_TFT_EN + #define PIN_TFT_EN -1 +#endif + +#ifndef PIN_TFT_BL + #define PIN_TFT_BL -1 +#endif + +#ifndef PIN_TFT_EN_ACTIVE + #define PIN_TFT_EN_ACTIVE LOW +#endif + +#ifndef PIN_TFT_BL_ACTIVE + #define PIN_TFT_BL_ACTIVE HIGH +#endif + +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 0 +#endif + +#ifndef NV3001B_SCREEN_WIDTH + #define NV3001B_SCREEN_WIDTH 220 +#endif + +#ifndef NV3001B_SCREEN_HEIGHT + #define NV3001B_SCREEN_HEIGHT 128 +#endif + +#ifndef DISPLAY_SCALE_X + #define DISPLAY_SCALE_X ((float)NV3001B_SCREEN_WIDTH / NV3001B_LOGICAL_WIDTH) +#endif + +#ifndef DISPLAY_SCALE_Y + #define DISPLAY_SCALE_Y ((float)NV3001B_SCREEN_HEIGHT / NV3001B_LOGICAL_HEIGHT) +#endif + +#define NV3001B_SWRESET 0x01 +#define NV3001B_SLPOUT 0x11 +#define NV3001B_DISPON 0x29 +#define NV3001B_CASET 0x2A +#define NV3001B_RASET 0x2B +#define NV3001B_RAMWR 0x2C +#define NV3001B_MADCTL 0x36 +#define NV3001B_COLMOD 0x3A + +#define NV3001B_MADCTL_MY 0x80 +#define NV3001B_MADCTL_MX 0x40 +#define NV3001B_MADCTL_MV 0x20 +#define NV3001B_MADCTL_RGB 0x00 + +#ifndef NV3001B_TEXT_SIZE1_SCALE_X + #define NV3001B_TEXT_SIZE1_SCALE_X 1 +#endif + +#ifndef NV3001B_TEXT_SIZE1_SCALE_Y + #define NV3001B_TEXT_SIZE1_SCALE_Y 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_X + #define NV3001B_TEXT_SIZE2_SCALE_X 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_Y + #define NV3001B_TEXT_SIZE2_SCALE_Y 3 +#endif + +static uint16_t mapColor(DisplayDriver::Color c) { + switch (c) { + case DisplayDriver::DARK: return 0x0000; + case DisplayDriver::LIGHT: return 0xffff; + case DisplayDriver::RED: return 0xf800; + case DisplayDriver::GREEN: return 0x07e0; + case DisplayDriver::BLUE: return 0x001f; + case DisplayDriver::YELLOW: return 0xffe0; + case DisplayDriver::ORANGE: return 0xfd20; + default: return 0xffff; + } +} + +static int scaleX(int x) { + return (int)(x * DISPLAY_SCALE_X); +} + +static int scaleY(int y) { + return (int)(y * DISPLAY_SCALE_Y); +} + +static int scaleWidth(int x, int w) { + if (w <= 0) return 0; + int scaled = scaleX(x + w) - scaleX(x); + return scaled > 0 ? scaled : 1; +} + +static int scaleHeight(int y, int h) { + if (h <= 0) return 0; + int scaled = scaleY(y + h) - scaleY(y); + return scaled > 0 ? scaled : 1; +} + +static uint8_t nv3001bMADCTL(uint8_t rotation) { + uint8_t madctl; + switch (rotation & 3) { + case 0: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + case 1: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MX | NV3001B_MADCTL_RGB; + break; + case 2: + madctl = NV3001B_MADCTL_RGB; + break; + default: + madctl = NV3001B_MADCTL_MX | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + } + return madctl; +} + +static const uint8_t font5x7[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14, + 0x7f, 0x14, 0x7f, 0x14, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49, + 0x55, 0x22, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00, 0x41, 0x22, + 0x1c, 0x00, 0x14, 0x08, 0x3e, 0x08, 0x14, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x50, 0x30, 0x00, + 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02, + 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, 0x42, 0x7f, 0x40, 0x00, 0x42, 0x61, 0x51, 0x49, 0x46, 0x21, + 0x41, 0x45, 0x4b, 0x31, 0x18, 0x14, 0x12, 0x7f, 0x10, 0x27, 0x45, 0x45, 0x45, 0x39, 0x3c, 0x4a, + 0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03, 0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49, + 0x29, 0x1e, 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41, + 0x00, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06, + 0x32, 0x49, 0x79, 0x41, 0x3e, 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x3e, + 0x41, 0x41, 0x41, 0x22, 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x7f, 0x49, 0x49, 0x49, 0x41, 0x7f, 0x09, + 0x09, 0x09, 0x01, 0x3e, 0x41, 0x49, 0x49, 0x7a, 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, 0x41, 0x7f, + 0x41, 0x00, 0x20, 0x40, 0x41, 0x3f, 0x01, 0x7f, 0x08, 0x14, 0x22, 0x41, 0x7f, 0x40, 0x40, 0x40, + 0x40, 0x7f, 0x02, 0x0c, 0x02, 0x7f, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x3e, 0x41, 0x41, 0x41, 0x3e, + 0x7f, 0x09, 0x09, 0x09, 0x06, 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x7f, 0x09, 0x19, 0x29, 0x46, 0x46, + 0x49, 0x49, 0x49, 0x31, 0x01, 0x01, 0x7f, 0x01, 0x01, 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x1f, 0x20, + 0x40, 0x20, 0x1f, 0x3f, 0x40, 0x38, 0x40, 0x3f, 0x63, 0x14, 0x08, 0x14, 0x63, 0x07, 0x08, 0x70, + 0x08, 0x07, 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x7f, 0x41, 0x41, 0x00, 0x02, 0x04, 0x08, 0x10, + 0x20, 0x00, 0x41, 0x41, 0x7f, 0x00, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x00, 0x01, 0x02, 0x04, 0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x7f, 0x48, 0x44, 0x44, 0x38, 0x38, + 0x44, 0x44, 0x44, 0x20, 0x38, 0x44, 0x44, 0x48, 0x7f, 0x38, 0x54, 0x54, 0x54, 0x18, 0x08, 0x7e, + 0x09, 0x01, 0x02, 0x0c, 0x52, 0x52, 0x52, 0x3e, 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7d, + 0x40, 0x00, 0x20, 0x40, 0x44, 0x3d, 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00, 0x00, 0x41, 0x7f, 0x40, + 0x00, 0x7c, 0x04, 0x18, 0x04, 0x78, 0x7c, 0x08, 0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38, + 0x7c, 0x14, 0x14, 0x14, 0x08, 0x08, 0x14, 0x14, 0x18, 0x7c, 0x7c, 0x08, 0x04, 0x04, 0x08, 0x48, + 0x54, 0x54, 0x54, 0x20, 0x04, 0x3f, 0x44, 0x40, 0x20, 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x1c, 0x20, + 0x40, 0x20, 0x1c, 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x44, 0x28, 0x10, 0x28, 0x44, 0x0c, 0x50, 0x50, + 0x50, 0x3c, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x41, 0x36, 0x08, 0x00, 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00, 0x06, 0x09, 0x09, 0x06 +}; + +static int textPixelScaleX(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_X : NV3001B_TEXT_SIZE2_SCALE_X; +} + +static int textPixelScaleY(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_Y : NV3001B_TEXT_SIZE2_SCALE_Y; +} + +static void setupOptionalOutput(int pin, int level) { + if (pin < 0) return; + + pinMode(pin, OUTPUT); + digitalWrite(pin, level); +} + +static void writeOptionalPin(int pin, int level) { + if (pin < 0) return; + + digitalWrite(pin, level); +} + +void NV3001BDisplay::writeCommand(uint8_t cmd) { + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, LOW); + digitalWrite(PIN_TFT_CS, LOW); + spi.transfer(cmd); + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeBytes(const uint8_t* data, size_t len) { + if (!data || len == 0) return; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + for (size_t i = 0; i < len; i++) { + spi.transfer(data[i]); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeCommandData(uint8_t cmd, const uint8_t* data, size_t len) { + writeCommand(cmd); + writeBytes(data, len); +} + +void NV3001BDisplay::setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + uint16_t x2 = x + w - 1; + uint16_t y2 = y + h - 1; + uint8_t data[4]; + + data[0] = x >> 8; + data[1] = x & 0xff; + data[2] = x2 >> 8; + data[3] = x2 & 0xff; + writeCommandData(NV3001B_CASET, data, sizeof(data)); + + data[0] = y >> 8; + data[1] = y & 0xff; + data[2] = y2 >> 8; + data[3] = y2 & 0xff; + writeCommandData(NV3001B_RASET, data, sizeof(data)); + + writeCommand(NV3001B_RAMWR); +} + +void NV3001BDisplay::writeColor(uint16_t rgb, uint32_t count) { + uint8_t hi = rgb >> 8; + uint8_t lo = rgb & 0xff; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + while (count--) { + spi.transfer(hi); + spi.transfer(lo); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::initPanel() { +#define CMD0(C) do { writeCommand(C); } while (0) +#define CMD1(C, A) do { const uint8_t d[] = { A }; writeCommandData(C, d, sizeof(d)); } while (0) +#define CMD2(C, A, B) do { const uint8_t d[] = { A, B }; writeCommandData(C, d, sizeof(d)); } while (0) + + CMD0(NV3001B_SWRESET); + delay(120); + CMD1(0xFF, 0xA5); + CMD1(0x41, 0x00); + CMD1(0x50, 0x02); + CMD1(0x52, 0x6E); + CMD1(0x57, 0x02); + CMD1(0x46, 0x11); + CMD2(0x47, 0x00, 0x01); + CMD2(0x8F, 0x22, 0x03); + CMD1(0x9A, 0x78); + CMD1(0x9B, 0x78); + CMD1(0x9C, 0xA0); + CMD1(0x9D, 0x17); + CMD1(0x9E, 0xC1); + CMD1(0x83, 0x5A); + CMD1(0x84, 0xB6); + CMD1(0xFF, 0xA5); + CMD1(0x85, 0x5F); + CMD1(0x6E, 0x0F); + CMD1(0x7E, 0x0F); + CMD1(0x60, 0x00); + CMD1(0x70, 0x00); + CMD1(0x6D, 0x33); + CMD1(0x7D, 0x37); + CMD1(0x61, 0x09); + CMD1(0x71, 0x0A); + CMD1(0x6C, 0x2A); + CMD1(0x7C, 0x36); + CMD1(0x62, 0x11); + CMD1(0x72, 0x10); + CMD1(0x68, 0x4E); + CMD1(0x78, 0x4E); + CMD1(0x66, 0x36); + CMD1(0x76, 0x3C); + CMD1(0x1A, 0x1C); + CMD1(0x7B, 0x14); + CMD1(0x63, 0x0D); + CMD1(0x73, 0x0A); + CMD1(0x6A, 0x16); + CMD1(0x7A, 0x12); + CMD1(0x64, 0x0B); + CMD1(0x74, 0x0A); + CMD1(0x69, 0x08); + CMD1(0x79, 0x0A); + CMD1(0x65, 0x06); + CMD1(0x75, 0x07); + CMD1(0x67, 0x23); + CMD1(0x77, 0x44); + CMD1(0xE0, 0x00); + CMD1(0xE9, 0x30); + CMD1(0xEB, 0xB7); + CMD1(0xEC, 0x00); + CMD1(0xED, 0x11); + CMD1(0xF0, 0xB7); + CMD1(0x53, 0x04); + CMD1(0x54, 0x04); + CMD1(0x55, 0x40); + CMD1(0x56, 0x40); + CMD2(0xA0, 0x60, 0x01); + CMD1(0xA1, 0x84); + CMD1(0xA2, 0x85); + CMD2(0xAB, 0x00, 0x02); + CMD2(0xAC, 0x00, 0x06); + CMD2(0xAD, 0x00, 0x03); + CMD2(0xAE, 0x00, 0x07); + CMD1(0xC7, 0x01); + CMD1(0xB9, 0x82); + CMD1(0xBA, 0x83); + CMD1(0xBB, 0x00); + CMD1(0xBC, 0x81); + CMD1(0xBD, 0x02); + CMD1(0xBE, 0x01); + CMD1(0xBF, 0x04); + CMD1(0xC0, 0x03); + CMD1(0xC8, 0x55); + CMD1(0xC9, 0xC9); + CMD1(0xCA, 0xC8); + CMD1(0xCB, 0xCB); + CMD1(0xCC, 0xCA); + CMD1(0xCD, 0x55); + CMD1(0xCE, 0xCE); + CMD1(0xCF, 0xCD); + CMD1(0xD0, 0xD0); + CMD1(0xD1, 0xCF); + CMD1(0xF2, 0x46); + CMD1(0xA8, 0x04); + CMD1(0xA9, 0xB0); + CMD1(0xAA, 0xA3); + CMD1(0xB6, 0x00); + CMD1(0xB7, 0xB0); + CMD1(0xB8, 0xA3); + CMD1(0xC4, 0x03); + CMD1(0xC5, 0xB0); + CMD1(0xC6, 0xA3); + CMD1(0x80, 0x10); + CMD1(0xFF, 0x00); + CMD1(0x35, 0x00); + CMD0(NV3001B_SLPOUT); + delay(120); + CMD1(NV3001B_COLMOD, 0x05); + CMD1(NV3001B_MADCTL, nv3001bMADCTL(DISPLAY_ROTATION)); + CMD0(NV3001B_DISPON); + delay(10); + +#undef CMD0 +#undef CMD1 +#undef CMD2 +} + +void NV3001BDisplay::fillPhysicalRect(int x, int y, int w, int h) { + if (!is_on || w <= 0 || h <= 0) return; + + if (x < 0) { + w += x; + x = 0; + } + if (y < 0) { + h += y; + y = 0; + } + if (x + w > NV3001B_SCREEN_WIDTH) w = NV3001B_SCREEN_WIDTH - x; + if (y + h > NV3001B_SCREEN_HEIGHT) h = NV3001B_SCREEN_HEIGHT - y; + if (w <= 0 || h <= 0) return; + + setAddrWindow(x, y, w, h); + writeColor(color, (uint32_t)w * h); +} + +void NV3001BDisplay::drawChar(int x, int y, char ch) { + if (ch < 32 || ch > 127) ch = '?'; + + uint16_t index = (uint16_t)(ch - 32) * 5; + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + for (int col = 0; col < 5; col++) { + uint8_t line = pgm_read_byte(font5x7 + index + col); + for (int row = 0; row < 7; row++) { + if (line & (1 << row)) { + fillPhysicalRect(x + col * scale_x, y + row * scale_y, scale_x, scale_y); + } + } + } +} + +bool NV3001BDisplay::begin() { + if (is_on) return true; + + if (periph_power) periph_power->claim(); + + setupOptionalOutput(PIN_TFT_EN, PIN_TFT_EN_ACTIVE); + setupOptionalOutput(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + digitalWrite(PIN_TFT_CS, HIGH); + digitalWrite(PIN_TFT_DC, HIGH); + delay(20); + + spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); + if (PIN_TFT_RST >= 0) { + pinMode(PIN_TFT_RST, OUTPUT); + digitalWrite(PIN_TFT_RST, HIGH); + delay(10); + digitalWrite(PIN_TFT_RST, LOW); + delay(20); + digitalWrite(PIN_TFT_RST, HIGH); + delay(120); + } + + initPanel(); + is_on = true; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; + writeOptionalPin(PIN_TFT_BL, PIN_TFT_BL_ACTIVE); + return true; +} + +void NV3001BDisplay::turnOn() { + begin(); +} + +void NV3001BDisplay::turnOff() { + if (!is_on) return; + + writeOptionalPin(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + writeOptionalPin(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE); + is_on = false; + if (periph_power) periph_power->release(); +} + +void NV3001BDisplay::clear() { + uint16_t saved = color; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = saved; +} + +void NV3001BDisplay::startFrame(Color bkg) { + color = mapColor(bkg); + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; +} + +void NV3001BDisplay::setTextSize(int sz) { + text_size = sz < 1 ? 1 : sz; +} + +void NV3001BDisplay::setColor(Color c) { + color = mapColor(c); +} + +void NV3001BDisplay::setCursor(int x, int y) { + cursor_x = scaleX(x); + cursor_y = scaleY(y); +} + +void NV3001BDisplay::print(const char* str) { + if (!str || !is_on) return; + + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + while (*str) { + if (*str == '\n') { + cursor_x = 0; + cursor_y += 8 * scale_y; + } else if (*str == '\r') { + cursor_x = 0; + } else { + drawChar(cursor_x, cursor_y, *str); + cursor_x += 6 * scale_x; + } + str++; + } +} + +void NV3001BDisplay::fillRect(int x, int y, int w, int h) { + fillPhysicalRect(scaleX(x), scaleY(y), scaleWidth(x, w), scaleHeight(y, h)); +} + +void NV3001BDisplay::drawRect(int x, int y, int w, int h) { + int x1 = scaleX(x); + int y1 = scaleY(y); + int sw = scaleWidth(x, w); + int sh = scaleHeight(y, h); + + fillPhysicalRect(x1, y1, sw, 1); + fillPhysicalRect(x1, y1 + sh - 1, sw, 1); + fillPhysicalRect(x1, y1, 1, sh); + fillPhysicalRect(x1 + sw - 1, y1, 1, sh); +} + +void NV3001BDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + if (!bits || !is_on) return; + + int byte_width = (w + 7) / 8; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + uint8_t byte = pgm_read_byte(bits + j * byte_width + i / 8); + if (byte & (0x80 >> (i & 7))) { + fillPhysicalRect(scaleX(x + i), scaleY(y + j), scaleWidth(x + i, 1), scaleHeight(y + j, 1)); + } + } + } +} + +uint16_t NV3001BDisplay::getTextWidth(const char* str) { + if (!str) return 0; + + uint16_t len = 0; + while (str[len] && str[len] != '\n' && str[len] != '\r') len++; + return (uint16_t)((len * 6 * textPixelScaleX(text_size)) / DISPLAY_SCALE_X); +} + +void NV3001BDisplay::endFrame() { +} diff --git a/src/helpers/ui/NV3001BDisplay.h b/src/helpers/ui/NV3001BDisplay.h new file mode 100644 index 000000000..98cdaae82 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.h @@ -0,0 +1,68 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include + +#ifndef NV3001B_LOGICAL_WIDTH + #define NV3001B_LOGICAL_WIDTH 128 +#endif + +#ifndef NV3001B_LOGICAL_HEIGHT + #define NV3001B_LOGICAL_HEIGHT 64 +#endif + +#ifndef NV3001B_PANEL_WIDTH + #define NV3001B_PANEL_WIDTH 128 +#endif + +#ifndef NV3001B_PANEL_HEIGHT + #define NV3001B_PANEL_HEIGHT 220 +#endif + +#ifndef NV3001B_SPI_HOST + #define NV3001B_SPI_HOST HSPI +#endif + +class NV3001BDisplay : public DisplayDriver { + SPIClass spi; + RefCountedDigitalPin* periph_power; + bool is_on = false; + uint16_t color = 0xffff; + uint8_t text_size = 1; + int cursor_x = 0; + int cursor_y = 0; + + void writeCommand(uint8_t cmd); + void writeBytes(const uint8_t* data, size_t len); + void writeCommandData(uint8_t cmd, const uint8_t* data, size_t len); + void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void writeColor(uint16_t rgb, uint32_t count); + void fillPhysicalRect(int x, int y, int w, int h); + void initPanel(); + void drawChar(int x, int y, char ch); + +public: + NV3001BDisplay(RefCountedDigitalPin* power = nullptr) : + DisplayDriver(NV3001B_LOGICAL_WIDTH, NV3001B_LOGICAL_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { } + + bool begin(); + static const char* driverName() { return "NV3001B"; } + static uint16_t physicalWidth() { return NV3001B_PANEL_WIDTH; } + static uint16_t physicalHeight() { return NV3001B_PANEL_HEIGHT; } + + bool isOn() override { return is_on; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp new file mode 100644 index 000000000..37188e9ef --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -0,0 +1,66 @@ +#include "HeltecRC32Board.h" + +void HeltecRC32Board::begin() { + ESP32Board::begin(); + + pinMode(PIN_ADC_CTRL, OUTPUT); + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); + +#ifdef SENSOR_RST_PIN + pinMode(SENSOR_RST_PIN, OUTPUT); + digitalWrite(SENSOR_RST_PIN, HIGH); +#endif + +#ifdef LED_POWER + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LOW); +#endif + + periph_power.begin(); + vext_power.begin(); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1L << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecRC32Board::powerOff() { + enterDeepSleep(0); +} + +void HeltecRC32Board::onBeforeTransmit() { + digitalWrite(P_LORA_TX_LED, HIGH); +} + +void HeltecRC32Board::onAfterTransmit() { + digitalWrite(P_LORA_TX_LED, LOW); +} + +uint16_t HeltecRC32Board::getBattMilliVolts() { + analogReadResolution(12); + digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); + delay(10); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogReadMilliVolts(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * raw); +} + +bool HeltecRC32Board::setAdcMultiplier(float multiplier) { + adc_mult = multiplier == 0.0f ? ADC_MULTIPLIER : multiplier; + return true; +} + +const char* HeltecRC32Board::getManufacturerName() const { + return "Heltec RC32"; +} diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h new file mode 100644 index 000000000..256cb964c --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER 4.9f +#endif + +class HeltecRC32Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + RefCountedDigitalPin vext_power; + + HeltecRC32Board() : + periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), + vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + void begin(); + void onBeforeTransmit() override; + void onAfterTransmit() override; + void powerOff() override; + uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override; + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_rc32/pins_arduino.h b/variants/heltec_rc32/pins_arduino.h new file mode 100644 index 000000000..8ea3a0c15 --- /dev/null +++ b/variants/heltec_rc32/pins_arduino.h @@ -0,0 +1,60 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 18; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 12; +static const uint8_t MISO = 13; +static const uint8_t SCK = 11; + +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; + +static const uint8_t RST_LoRa = 9; +static const uint8_t BUSY_LoRa = 1; +static const uint8_t DIO1_LoRa = 14; + +#endif diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini new file mode 100644 index 000000000..9284650bd --- /dev/null +++ b/variants/heltec_rc32/platformio.ini @@ -0,0 +1,346 @@ +[Heltec_RC32] +extends = esp32_base +board = heltec-rc32 +board_build.partitions = default_16MB.csv +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_rc32 + -I src/helpers/ui + -D HELTEC_RC32 + -D USE_SX1262 + -D ESP32_CPU_FREQ=160 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=10 + -D P_LORA_RESET=9 + -D P_LORA_BUSY=1 + -D P_LORA_SCLK=11 + -D P_LORA_MISO=13 + -D P_LORA_MOSI=12 + -D LORA_TX_POWER=22 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=18 + -D PIN_VEXT_EN=3 + -D PIN_VEXT_EN_ACTIVE=HIGH + -D SENSOR_POWER_CTRL_PIN=46 + -D SENSOR_POWER_ON=HIGH + -D SENSOR_RST_PIN=2 + -D P_LORA_TX_LED=47 + -D PIN_BUZZER=48 + -D PIN_TFT_SCL=17 + -D PIN_TFT_SDA=38 + -D PIN_TFT_CS=39 + -D PIN_TFT_DC=16 + -D PIN_TFT_RST=4 + -D PIN_TFT_EN=6 + -D PIN_TFT_EN_ACTIVE=LOW + -D PIN_TFT_BL=5 + -D PIN_TFT_BL_ACTIVE=HIGH + -D SPI_FREQUENCY=8000000 + -D PIN_GPS_TX=44 + -D PIN_GPS_RX=43 + -D PIN_GPS_EN=45 + -D PIN_GPS_EN_ACTIVE=HIGH + -D PIN_GPS_RESET=40 + -D PIN_GPS_RESET_ACTIVE=LOW + -D PIN_GPS_PPS=41 + -D GPS_BAUD_RATE=9600 + -D ENV_INCLUDE_GPS=1 + -D PIN_ADC_CTRL=15 + -D PIN_VBAT_READ=7 + -D ADC_CTRL_ENABLED=HIGH + -D ADC_MULTIPLIER=4.9 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_rc32> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[Heltec_RC32_with_display] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D HELTEC_RC32_WITH_DISPLAY + -D DISPLAY_CLASS=NV3001BDisplay +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + +lib_deps = + ${Heltec_RC32.lib_deps} + +[env:heltec_rc32_without_display_repeater] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_without_display_repeater_bridge_espnow] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -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 = ${Heltec_RC32.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_room_server] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_companion_radio_usb] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_ble] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.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 AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_wifi] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -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 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_sensor] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_repeater] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_repeater_bridge_espnow] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -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 = ${Heltec_RC32_with_display.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_room_server] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_terminal_chat] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_rc32_companion_radio_usb] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_ble] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_wifi] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.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 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_sensor] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_kiss_modem] +extends = Heltec_RC32 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp new file mode 100644 index 000000000..6c88d2bb5 --- /dev/null +++ b/variants/heltec_rc32/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecRC32Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi(FSPI); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h new file mode 100644 index 000000000..ae6925980 --- /dev/null +++ b/variants/heltec_rc32/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS +#include +#ifdef HELTEC_RC32_WITH_DISPLAY +#include +#else +#include +#endif +#endif + +extern HeltecRC32Board 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_rc32/variant.h b/variants/heltec_rc32/variant.h new file mode 100644 index 000000000..d510ab9b6 --- /dev/null +++ b/variants/heltec_rc32/variant.h @@ -0,0 +1,51 @@ +#ifndef _VARIANT_HELTEC_RC32_ +#define _VARIANT_HELTEC_RC32_ + +#define BUTTON_PIN 0 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define PIN_GPS_EN 45 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET 40 +#define GPS_RESET_MODE LOW +#define PIN_GPS_PPS 41 + +#define I2C_SCL 18 +#define I2C_SDA 21 +#define SENSOR_INT_PIN 42 +#define SENSOR_RST_PIN 2 +#define SENSOR_POWER_CTRL_PIN 46 +#define SENSOR_POWER_ON HIGH +#define PERIPHERAL_WARMUP_MS 100 + +#define VEXT_ENABLE 3 +#define VEXT_ON_VALUE HIGH + +#define USE_SX1262 +#define LORA_SCK 11 +#define LORA_MISO 13 +#define LORA_MOSI 12 +#define LORA_CS 10 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_DIO1 14 +#define LORA_RESET 9 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 1 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC_CHANNEL_6 +#define ADC_CTRL 15 +#define ADC_CTRL_ENABLED HIGH +#define ADC_MULTIPLIER 4.9 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 + +#endif From 4605d3b6ce7a89806a35acad9359c0caf7d2a27e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 9 Jul 2026 16:31:28 +0200 Subject: [PATCH 090/191] 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 82f886c32..686b09ecd 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 cc541e49f..0c6f828b0 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 bee252743..e6f3270be 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 5df632020..e579e91ca 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 091/191] 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 36c1d19d5..4667e0038 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 092/191] 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 352184b86..3525d9b49 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 093/191] 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 6fe904366..035476e70 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 094/191] 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 ad4573548..ec8e7a17d 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 037192461..0b9de40ca 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 3525d9b49..14116c6f3 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 095/191] 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 73842d9ee..af09eff62 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 eec466d3a..6b06259b9 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 096/191] 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 356edfee6..617f92405 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 6fe904366..012afe469 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 097/191] 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 47cc0ab8c..f3ada1338 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 098/191] 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 000000000..31ebdbb91 --- /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 a310539bc..af08fddb0 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 f7d20b8ab..98d693958 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 cb56ff8a7..9822a67df 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 000000000..bf558639d --- /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 000000000..d5f68e055 --- /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 000000000..1886eea03 --- /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 000000000..a7171726a --- /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 000000000..3cbd7d758 --- /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 000000000..f083a9f1b --- /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 000000000..4ac6a7827 --- /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 000000000..37f8b0763 --- /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 7e95c52972f237dd4b238efef4a5af13a2b72ea4 Mon Sep 17 00:00:00 2001 From: Will Dillon Date: Mon, 13 Jul 2026 05:44:55 -0700 Subject: [PATCH 099/191] Add StreamSensor allocation for GroupData Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore. Let me know when and how to demonstrate. --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 2ccf950f4..b8cc22a21 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -18,6 +18,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | | 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ | +| 0130 - 013F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 1bdacb324710f9f8c14e9dc50e8c79aab36e663b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 13 Jul 2026 23:27:50 +1000 Subject: [PATCH 100/191] * trying to get Heltec Tracker and Tracker V2 working --- src/helpers/ui/ST7735Display.cpp | 9 +++++++-- variants/heltec_tracker/platformio.ini | 2 +- variants/heltec_tracker_v2/platformio.ini | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index aad12d1b4..cb5737e18 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -102,7 +102,12 @@ static TFT_eSPI lcd = TFT_eSPI(160, 80); static uint32_t curr_color; -#define _spi (&SPI1) +#if defined(HELTEC_LORA_V3) || defined(HELTEC_TRACKER_V2) + static SPIClass tft_spi(SPI3_HOST); + #define _spi (&tft_spi) +#else + #define _spi (&SPI1) +#endif SPISettings _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); @@ -434,7 +439,7 @@ bool ST7735Display::begin() { pinMode(PIN_TFT_LEDA_CTL, OUTPUT); #ifdef ESP_PLATFORM - _spi->begin(_clk,_miso,_mosi,-1); + _spi->begin(PIN_TFT_SCL, -1 /* _miso */, PIN_TFT_SDA /* _mosi */, -1); #else _spi->begin(); #endif diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 69293d707..07d2e987a 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -47,7 +47,7 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 [env:Heltec_Wireless_Tracker_companion_radio_usb] extends = Heltec_tracker_base diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d57c2113f..f1fe9dd05 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -55,7 +55,7 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 [env:heltec_tracker_v2_repeater] extends = Heltec_tracker_v2 From d30d8ed7483d45f5e0faa53e9e56a846e67e5145 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 14 Jul 2026 15:46:36 +1000 Subject: [PATCH 101/191] * HSPI fix --- src/helpers/ui/ST7735Display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index cb5737e18..cdd15aeb0 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -103,7 +103,7 @@ static TFT_eSPI lcd = TFT_eSPI(160, 80); static uint32_t curr_color; #if defined(HELTEC_LORA_V3) || defined(HELTEC_TRACKER_V2) - static SPIClass tft_spi(SPI3_HOST); + static SPIClass tft_spi(HSPI); #define _spi (&tft_spi) #else #define _spi (&SPI1) From 4d0ba1516c539c86d2ef8a481b2acc943ea1e167 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 14 Jul 2026 17:23:13 +0800 Subject: [PATCH 102/191] Remove the watchdog feeding operation from each iteration of the loop to reduce power consumption. --- examples/simple_repeater/main.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7532eb47e..72bf8d57f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -160,9 +160,6 @@ void loop() { external_watchdog.loop(); #endif if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { -#ifdef HAS_EXTERNAL_WATCHDOG - external_watchdog.feed(); -#endif #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else From a9d25740390a5ad425cd71057e107fb3821fd4b2 Mon Sep 17 00:00:00 2001 From: Nick Dunklee Date: Tue, 14 Jul 2026 08:35:27 -0600 Subject: [PATCH 103/191] feat: add more crypto After soaking for a bit on the adverts without issue on multiple nodes, I added more hardware crypto. Supported nodes is unchanged in this PR addition, but if others can verify, they can easily be added. Some info on the CC310: https://docs.nordicsemi.com/r/bundle/ps_nrf9151/page/cryptocell.html **Added:** - AES-128 packet encryption/decryption now use hardware crypto - HMAC-SHA-256 authentication now uses hardware crypto - ACK hash computation and channel ID derivation now use hardware crypto - RNG (random number generator) now uses hardware crypto rather than radio noise + weak software RNG (which can have issues if there's no surrounding radio noise.) NIST SP 800-90B certified. - Runs hardware self-tests on startup - Runs continuous health tests during operation - Uses thermal noise/shot noise for randomness **Unchanged:** - calcSharedSecret remains software - it would be a split hw/sw solution and added complexity for likely not a lot of gains. This only happens when establishing a new contact, so not too frequent to be worth it. - ed25519_create_keypair remains software. This is only called when a node is first initialized. It does use the hardware RNG change, however, so better randomization. Tested on (so far): - Heltec t096 Build test on: - Heltec t096 companion ble - t1000e companion ble - RAK 4631 repeater - RAK 3401 companion BLE - Heltec v3 companion wifi --- src/Utils.cpp | 98 ++++++++++++++++++++++++- src/helpers/radiolib/RadioLibWrappers.h | 11 +++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720a..9cd44ce24 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -2,6 +2,13 @@ #include #include +#ifdef USE_CC310_ED25519 +#include +#include "nrf_cc310/include/crys_hash.h" +#include "nrf_cc310/include/crys_hmac.h" +#include "nrf_cc310/include/ssi_aes.h" +#endif + #ifdef ARDUINO #include #endif @@ -15,19 +22,58 @@ uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) { } void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) { +#ifdef USE_CC310_ED25519 + static CRYS_HASH_Result_t result; + nRFCrypto.begin(); + CRYS_HASH(CRYS_HASH_SHA256_mode, (uint8_t*)msg, (size_t)msg_len, result); + nRFCrypto.end(); + memcpy(hash, result, hash_len); +#else SHA256 sha; sha.update(msg, msg_len); sha.finalize(hash, hash_len); +#endif } void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len) { +#ifdef USE_CC310_ED25519 + static CRYS_HASHUserContext_t ctx; + static CRYS_HASH_Result_t result; + nRFCrypto.begin(); + CRYS_HASH_Init(&ctx, CRYS_HASH_SHA256_mode); + CRYS_HASH_Update(&ctx, (uint8_t*)frag1, (size_t)frag1_len); + CRYS_HASH_Update(&ctx, (uint8_t*)frag2, (size_t)frag2_len); + CRYS_HASH_Finish(&ctx, result); + nRFCrypto.end(); + memcpy(hash, result, hash_len); +#else SHA256 sha; sha.update(frag1, frag1_len); sha.update(frag2, frag2_len); sha.finalize(hash, hash_len); +#endif } int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { +#ifdef USE_CC310_ED25519 + static SaSiAesUserContext_t ctx; + SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE }; + uint8_t* dp = dest; + const uint8_t* sp = src; + size_t dummy_out = 0; + + nRFCrypto.begin(); + SaSi_AesInit(&ctx, SASI_AES_DECRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE); + SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData)); + while (sp - src < src_len) { + SaSi_AesBlock(&ctx, (uint8_t*)sp, 16, dp); + dp += 16; sp += 16; + } + SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out); + SaSi_AesFree(&ctx); + nRFCrypto.end(); + return sp - src; +#else AES128 aes; uint8_t* dp = dest; const uint8_t* sp = src; @@ -39,9 +85,34 @@ int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s } return sp - src; // will always be multiple of 16 +#endif } int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { +#ifdef USE_CC310_ED25519 + static SaSiAesUserContext_t ctx; + SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE }; + uint8_t* dp = dest; + size_t dummy_out = 0; + + nRFCrypto.begin(); + SaSi_AesInit(&ctx, SASI_AES_ENCRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE); + SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData)); + while (src_len >= 16) { + SaSi_AesBlock(&ctx, (uint8_t*)src, 16, dp); + dp += 16; src += 16; src_len -= 16; + } + if (src_len > 0) { // remaining partial block — zero-pad to 16 bytes + uint8_t tmp[16] = {}; + memcpy(tmp, src, src_len); + SaSi_AesBlock(&ctx, tmp, 16, dp); + dp += 16; + } + SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out); + SaSi_AesFree(&ctx); + nRFCrypto.end(); + return dp - dest; +#else AES128 aes; uint8_t* dp = dest; @@ -58,15 +129,27 @@ int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s dp += 16; } return dp - dest; // will always be multiple of 16 +#endif } int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len); +#ifdef USE_CC310_ED25519 + static CRYS_HMACUserContext_t hmac_ctx; + static CRYS_HASH_Result_t hmac_result; + nRFCrypto.begin(); + CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE); + CRYS_HMAC_Update(&hmac_ctx, dest + CIPHER_MAC_SIZE, enc_len); + CRYS_HMAC_Finish(&hmac_ctx, hmac_result); + nRFCrypto.end(); + memcpy(dest, hmac_result, CIPHER_MAC_SIZE); +#else SHA256 sha; sha.resetHMAC(shared_secret, PUB_KEY_SIZE); sha.update(dest + CIPHER_MAC_SIZE, enc_len); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE); +#endif return CIPHER_MAC_SIZE + enc_len; } @@ -75,12 +158,25 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes uint8_t hmac[CIPHER_MAC_SIZE]; +#ifdef USE_CC310_ED25519 + { + static CRYS_HMACUserContext_t hmac_ctx; + static CRYS_HASH_Result_t hmac_result; + nRFCrypto.begin(); + CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE); + CRYS_HMAC_Update(&hmac_ctx, (uint8_t*)(src + CIPHER_MAC_SIZE), src_len - CIPHER_MAC_SIZE); + CRYS_HMAC_Finish(&hmac_ctx, hmac_result); + nRFCrypto.end(); + memcpy(hmac, hmac_result, CIPHER_MAC_SIZE); + } +#else { SHA256 sha; sha.resetHMAC(shared_secret, PUB_KEY_SIZE); sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); } +#endif if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); } @@ -150,4 +246,4 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep return num; } -} \ No newline at end of file +} diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3091832f1..f390199f7 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,10 @@ #include #include +#ifdef USE_CC310_ED25519 +#include +#endif + class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; @@ -80,8 +84,15 @@ public: RadioNoiseListener(PhysicalLayer& radio): _radio(&radio) { } void random(uint8_t* dest, size_t sz) override { +#ifdef USE_CC310_ED25519 + // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. + nRFCrypto.begin(); + nRFCrypto.Random.generate(dest, (uint16_t)sz); + nRFCrypto.end(); +#else for (int i = 0; i < sz; i++) { dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF); } +#endif } }; From 2af1e6d099435bd2500a87e9a2ee584a00df398e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 01:23:57 +1000 Subject: [PATCH 104/191] * first draft of new ConfigSerializer. (simple embedded JSON prefs) --- examples/simple_repeater/MyMesh.cpp | 1 - examples/simple_room_server/MyMesh.cpp | 1 - examples/simple_sensor/SensorMesh.cpp | 1 - src/helpers/CommonCLI.cpp | 84 ++------ src/helpers/CommonCLI.h | 122 ++++++++++- src/helpers/ConfigSerializer.cpp | 278 +++++++++++++++++++++++++ src/helpers/ConfigSerializer.h | 67 ++++++ 7 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 src/helpers/ConfigSerializer.cpp create mode 100644 src/helpers/ConfigSerializer.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b66e19522..09f74cbea 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -872,7 +872,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #endif // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e808..1a778d4a6 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -628,7 +628,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc recv_pkt_region = NULL; // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // off by default, was 10.0 _prefs.tx_delay_factor = 0.5f; // was 0.25f; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 59c9aa090..17f8e323e 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -710,7 +710,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise set_radio_at = revert_radio_at = 0; // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c95e3e34b..56a46d467 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -28,16 +28,21 @@ static bool isValidName(const char *n) { } void CommonCLI::loadPrefs(FILESYSTEM* fs) { - if (fs->exists("/com_prefs")) { - loadPrefsInt(fs, "/com_prefs"); // new filename - } else if (fs->exists("/node_prefs")) { - loadPrefsInt(fs, "/node_prefs"); - savePrefs(fs); // save to new filename - fs->remove("/node_prefs"); // remove old + if (fs->exists("/ser_prefs")) { + File file = fs->open("/ser_prefs"); + if (file) { + _prefs->loadSerial(file); // new Serial prefs + file.close(); + } + } else if (fs->exists("/com_prefs")) { + loadPrefsInt(fs, "/com_prefs"); + if (savePrefs(fs)) { // save to new Serial prefs + // fs->remove("/com_prefs"); // remove old + } } } -void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { +void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy prefs loader #if defined(RP2040_PLATFORM) File file = fs->open(filename, "r"); #else @@ -130,70 +135,21 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } } -void CommonCLI::savePrefs(FILESYSTEM* fs) { +bool CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/com_prefs"); - File file = fs->open("/com_prefs", FILE_O_WRITE); + fs->remove("/ser_prefs"); + File file = fs->open("/ser_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) - File file = fs->open("/com_prefs", "w"); + File file = fs->open("/ser_prefs", "w"); #else - File file = fs->open("/com_prefs", "w", true); + File file = fs->open("/ser_prefs", "w", true); #endif if (file) { - uint8_t pad[8]; - memset(pad, 0, sizeof(pad)); - - file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 - file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4 - file.write(pad, 4); // 36 - file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 - file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 - file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56 - file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72 - file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 - file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 - file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 - file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end) - file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 - file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 - file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 - file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 : 4 byte unused - file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 - file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 - file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 - file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 - file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 - file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 - file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 - file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 - file.write(pad, 1); // 123 - file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 - file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 - file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 - file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 - file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 - file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 - file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 - file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 - file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 - file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.write(pad, 3); // 153 - file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 - file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 - file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 - file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - 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 - 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 - file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 - + bool success = _prefs->saveSerial(file); file.close(); + return success; } + return false; } #define MIN_LOCAL_ADVERT_INTERVAL 60 diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf477..aab33d114 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -19,7 +20,9 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 -struct NodePrefs { // persisted to file +class NodePrefs : public ConfigSerializer { +public: + // in-memory backing data float airtime_factor; char node_name[32]; double node_lat, node_lon; @@ -65,6 +68,121 @@ struct NodePrefs { // persisted to file 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) + +private: + class RadioPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("freq", _parent->freq); + def("bw", _parent->bw); + def("sf", _parent->sf); + def("cr", _parent->cr); + def("cad", _parent->cad_enabled); + def("int_thr", _parent->interference_threshold); + def("rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->rx_boosted_gain); + def("tx", _parent->tx_power_dbm); + def("af", _parent->airtime_factor); + def("rxdelay", _parent->rx_delay_base); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); + def("hash_mode", _parent->path_hash_mode); + def("multi_ack", _parent->multi_acks); + } + public: + RadioPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RadioPrefs radio; + + class BridgePrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->bridge_enabled); // boolean + def("delay", _parent->bridge_delay); // milliseconds (default 500 ms) + def("src", _parent->bridge_pkt_src); // 0 = logTx, 1 = logRx (default logTx) + def("baud", _parent->bridge_baud); // 9600, 19200, 38400, 57600, 115200 (default 115200) + def("ch", _parent->bridge_channel); // 1-14 (ESP-NOW only) + def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); // for XOR encryption of bridge packets (ESP-NOW only) + } + public: + BridgePrefs(NodePrefs* parent) : _parent(parent) { } + }; + BridgePrefs bridge; + + class GPSPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->gps_enabled); // boolean + def("int", _parent->gps_interval); // interval in seconds + def("adv_loc", _parent->advert_loc_policy); + } + public: + GPSPrefs(NodePrefs* parent) : _parent(parent) { } + }; + GPSPrefs gps; + + class PowerPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("adc_mult", _parent->adc_multiplier); + def("pwr_sav_en", _parent->powersaving_enabled); + } + public: + PowerPrefs(NodePrefs* parent) : _parent(parent) { } + }; + PowerPrefs power; + + class RepeatPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("disable", _parent->disable_fwd); + def("f_max", _parent->flood_max); + def("f_max_uns", _parent->flood_max_unscoped); + def("f_max_adv", _parent->flood_max_advert); + def("loop", _parent->loop_detect); + } + public: + RepeatPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RepeatPrefs repeat; + + class RoomPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("rd_only", _parent->allow_read_only); + } + public: + RoomPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RoomPrefs room; + +protected: + void structure() override { + def("name", node_name, sizeof(node_name)); + def("pass", password, sizeof(password)); + def("guest", guest_password, sizeof(guest_password)); + def("owner", owner_info, sizeof(owner_info)); + def("adv_int", advert_interval); + def("f_adv_int", flood_advert_interval); + def("lat", node_lat); + def("lon", node_lon); + def("radio", radio); + def("bridge", bridge); + def("gps", gps); + def("repeat", repeat); + def("room", room); + def("power", power); + } + +public: + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { } }; class CommonCLICallbacks { @@ -139,7 +257,7 @@ public: : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } void loadPrefs(FILESYSTEM* _fs); - void savePrefs(FILESYSTEM* _fs); + bool savePrefs(FILESYSTEM* _fs); void handleCommand(uint32_t sender_timestamp, char* command, char* reply); uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data); }; diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp new file mode 100644 index 000000000..221fd197c --- /dev/null +++ b/src/helpers/ConfigSerializer.cpp @@ -0,0 +1,278 @@ +#include "ConfigSerializer.h" + +bool ConfigSerializer::saveSerial(Stream& s) { + Context context(&s, OP::WRITE); + _context = &context; // set the context for structure() call + s.print("{"); // root object + _first = true; + structure(); + if (s.print("}") != 1) context.success = false; // failure detect + _context = NULL; + return context.success; +} + +#define TOK_ERROR -1 +#define TOK_EOF 0 +#define TOK_KEY 1 +#define TOK_VALUE 2 +#define TOK_START_OBJ 3 +#define TOK_END_OBJ 4 +#define TOK_WHITESPACE 5 + +static bool is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} +static bool is_key_char(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} +static bool is_value_char(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; +} + +#define EXPECT_OPEN_BRACE 0 +#define EXPECT_KEY 1 +#define EXPECT_VAL_OR_OBJ 2 +#define EXPECT_STRING_VAL 3 +#define EXPECT_STRING_ESCAPE 4 +#define EXPECT_COMMA_OR_CLOSE 5 +#define EXPECT_COMMA_OR_KEY 6 +#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7 + +int ConfigSerializer::Context::readNext() { + char c; + if (pending) { + c = pending; + pending = 0; + } else { + if (_f->available() == 0) return TOK_EOF; + + int n = _f->read(); + if (n < 0) return TOK_EOF; + c = (char)n; + } + + switch (rd_mode) { + case EXPECT_OPEN_BRACE: + if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } + if (is_whitespace(c)) return TOK_WHITESPACE; + return TOK_ERROR; + + case EXPECT_COMMA_OR_KEY_OR_CLOSE: + if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } + case EXPECT_COMMA_OR_KEY: + if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } + case EXPECT_KEY: + if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } + if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; + if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + return TOK_ERROR; + + case EXPECT_VAL_OR_OBJ: + if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; + if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } + if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } + return TOK_ERROR; + + case EXPECT_STRING_ESCAPE: + if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + return TOK_ERROR; // unsupport escape + + case EXPECT_STRING_VAL: + if (c == '"') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } + if (c == '\\') { rd_mode = EXPECT_STRING_ESCAPE; return TOK_WHITESPACE; } + if (rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + return TOK_ERROR; + + case EXPECT_COMMA_OR_CLOSE: + if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } + if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } + if (is_whitespace(c)) return TOK_WHITESPACE; + return TOK_ERROR; + } + return TOK_ERROR; // unknown mode +} + +bool ConfigSerializer::loadSerial(Stream& s) { + Context context(&s, OP::READ); + _context = &context; // set the context for structure() call + // parse the Json file + uint8_t sp = 0; + int next_tok; + + while ((next_tok = context.readNext()) > TOK_EOF) { + if (next_tok == TOK_KEY) { + context.setKey(sp, context.getToken()); + } else if (next_tok == TOK_VALUE) { + _depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth) + structure(); + } else if (next_tok == TOK_START_OBJ) { + if (sp < CONFIF_MAX_DEPTH - 1) { + sp++; + } else { + Serial.printf("Error: max nesting reached"); // TODO: debug logging + context.success = false; + break; + } + } else if (next_tok == TOK_END_OBJ) { + if (sp > 0) { + sp--; + } else { + Serial.printf("Error: too many closing '}'"); // TODO: debug logging + context.success = false; + break; + } + } + } + if (sp != 0 || next_tok == TOK_ERROR) { + context.success = false; // unmatched { }, or other parse error + } + _context = NULL; + return context.success; +} + +void ConfigSerializer::writeComma() { + if (_first) { + _first = false; + } else { + _context->file()->print(","); // comma separated properties + } +} + +void ConfigSerializer::def(const char* key, char* value, size_t max_len) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":\""); + _context->file()->print(value); // TODO: escape quotes + _context->file()->print("\""); + } else { + if (_context->keyMatch(_depth, key)) { + strncpy(value, _context->getToken(), max_len - 1); + value[max_len - 1] = 0; + } + } +} + +void ConfigSerializer::def(const char* key, int32_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint32_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, int16_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((int32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint16_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((uint32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint8_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((uint32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, int8_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((int32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, double& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value, 6); // REVISIT: how many dec places? + } else { + if (_context->keyMatch(_depth, key)) { + value = atof(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, float& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value, 4); // REVISIT: how many dec places? + } else { + if (_context->keyMatch(_depth, key)) { + value = (float) atof(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":{"); + sub_obj._context = _context; // inherit the Context + sub_obj._first = true; + sub_obj.structure(); // recurse into sub object + if (_context->file()->print("}") != 1) _context->success = false; // failure detect + } else { + if (_context->keyMatch(_depth, key)) { + sub_obj._context = _context; // inherit the Context + sub_obj._depth = _depth + 1; + sub_obj.structure(); // recurse into sub object + } + } +} diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h new file mode 100644 index 000000000..2f1d2ae10 --- /dev/null +++ b/src/helpers/ConfigSerializer.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#ifndef CONFIF_MAX_DEPTH + #define CONFIF_MAX_DEPTH 8 +#endif + +#ifndef CONFIG_MAX_KEYLEN + #define CONFIG_MAX_KEYLEN 16 +#endif + +#ifndef CONFIG_MAX_TOKEN_LEN + #define CONFIG_MAX_TOKEN_LEN 128 +#endif + +class ConfigSerializer { + bool _first; + int8_t _depth; + + enum OP { READ, WRITE }; + + class Context { + Stream* _f; + OP _op; + uint8_t rd_len; + uint8_t rd_mode; + char pending; + char rd_buf[CONFIG_MAX_TOKEN_LEN]; + char _keys[CONFIF_MAX_DEPTH][CONFIG_MAX_KEYLEN]; + + public: + bool success = true; + Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; } + OP op() const { return _op; } + Stream* file() const { return _f; } + int readNext(); + const char* getToken() const { return rd_buf; } + bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } + void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } + }; + + Context* _context = NULL; + + void writeComma(); + +protected: + ConfigSerializer() { } + + void def(const char* key, char* value, size_t max_len); // max_len inclusive of null + void def(const char* key, int32_t& value); + void def(const char* key, int16_t& value); + void def(const char* key, int8_t& value); + void def(const char* key, uint32_t& value); + void def(const char* key, uint16_t& value); + void def(const char* key, uint8_t& value); + void def(const char* key, float& value); + void def(const char* key, double& value); + void def(const char* key, bool& value); + void def(const char* key, ConfigSerializer& sub_obj); + + virtual void structure() = 0; + +public: + bool loadSerial(Stream& s); + bool saveSerial(Stream& s); +}; From 15e259c57228bf6a2be92f820b2e312f954eecb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= Date: Mon, 6 Jul 2026 01:33:01 +0200 Subject: [PATCH 105/191] Added MCU temp sensor to the companion Fixes #2896 --- examples/companion_radio/MyMesh.cpp | 10 ++++++++++ src/helpers/NRF52Board.cpp | 27 ++++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5fb9bf9d3..a78fd29a2 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -657,6 +657,11 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim // query other sensors -- target specific sensors.querySensors(permissions, telemetry); + float temperature = board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } + memcpy(reply, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') @@ -1640,6 +1645,11 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + float temperature = board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } + // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index beee32126..46dbd329f 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -280,16 +280,29 @@ void NRF52Board::sleep(uint32_t secs) { // Temperature from NRF52 MCU float NRF52Board::getMCUTemperature() { - NRF_TEMP->TASKS_START = 1; // Start temperature measurement - - long startTime = millis(); - while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us - if(millis() - startTime > 5) { // To wait 5ms just in case - NRF_TEMP->TASKS_STOP = 1; + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + uint32_t err_code; + int32_t temp; + err_code = sd_temp_get(&temp); + if (err_code == NRF_SUCCESS) { + return (float)temp * 0.25f; + } else { return NAN; } + } else { + NRF_TEMP->TASKS_START = 1; // Start temperature measurement + + long startTime = millis(); + while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us + if(millis() - startTime > 5) { // To wait 5ms just in case + NRF_TEMP->TASKS_STOP = 1; + return NAN; + } + } } - + NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units From 63f4b173daa9371f296facd03e2854df5ed85f0d Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 13:21:44 +1000 Subject: [PATCH 106/191] * misc fixes --- src/helpers/ConfigSerializer.cpp | 46 +++++++++++++++++++++++++++----- src/helpers/ConfigSerializer.h | 6 ++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 221fd197c..766733099 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -97,10 +97,10 @@ int ConfigSerializer::Context::readNext() { bool ConfigSerializer::loadSerial(Stream& s) { Context context(&s, OP::READ); _context = &context; // set the context for structure() call - // parse the Json file - uint8_t sp = 0; + uint8_t sp = 0; // object nesting stack pointer int next_tok; + // parse the Json file while ((next_tok = context.readNext()) > TOK_EOF) { if (next_tok == TOK_KEY) { context.setKey(sp, context.getToken()); @@ -108,7 +108,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { _depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth) structure(); } else if (next_tok == TOK_START_OBJ) { - if (sp < CONFIF_MAX_DEPTH - 1) { + if (sp < CONFIG_MAX_DEPTH - 1) { sp++; } else { Serial.printf("Error: max nesting reached"); // TODO: debug logging @@ -145,7 +145,20 @@ void ConfigSerializer::def(const char* key, char* value, size_t max_len) { writeComma(); _context->file()->print(key); _context->file()->print(":\""); - _context->file()->print(value); // TODO: escape quotes + char c; + while ((c = *value++) != 0) { // TODO: handle UTF-8 encoding + if (c == '"') { + _context->file()->print("\\\""); + } else if (c == '\\') { + _context->file()->print("\\\\"); + } else if (c == '\n') { + _context->file()->print("\\n"); + } else if (c == '\r') { + _context->file()->print("\\r"); + } else { + _context->file()->print(c); + } + } _context->file()->print("\""); } else { if (_context->keyMatch(_depth, key)) { @@ -233,12 +246,29 @@ void ConfigSerializer::def(const char* key, int8_t& value) { } } +void ConfigSerializer::def(const char* key, bool& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value ? "true" : "false"); + } else { + if (_context->keyMatch(_depth, key)) { + value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number + } + } +} + void ConfigSerializer::def(const char* key, double& value) { if (_context->op() == OP::WRITE) { writeComma(); _context->file()->print(key); _context->file()->print(":"); - _context->file()->print(value, 6); // REVISIT: how many dec places? + if (value == 0.0) { + _context->file()->print("0"); // shorter encoding + } else { + _context->file()->print(value, 6); // REVISIT: how many dec places? + } } else { if (_context->keyMatch(_depth, key)) { value = atof(_context->getToken()); @@ -251,7 +281,11 @@ void ConfigSerializer::def(const char* key, float& value) { writeComma(); _context->file()->print(key); _context->file()->print(":"); - _context->file()->print(value, 4); // REVISIT: how many dec places? + if (value == 0.0f) { + _context->file()->print("0"); // shorter encoding + } else { + _context->file()->print(value, 4); // REVISIT: how many dec places? + } } else { if (_context->keyMatch(_depth, key)) { value = (float) atof(_context->getToken()); diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 2f1d2ae10..5f0dc4888 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -2,8 +2,8 @@ #include -#ifndef CONFIF_MAX_DEPTH - #define CONFIF_MAX_DEPTH 8 +#ifndef CONFIG_MAX_DEPTH + #define CONFIG_MAX_DEPTH 8 #endif #ifndef CONFIG_MAX_KEYLEN @@ -27,7 +27,7 @@ class ConfigSerializer { uint8_t rd_mode; char pending; char rd_buf[CONFIG_MAX_TOKEN_LEN]; - char _keys[CONFIF_MAX_DEPTH][CONFIG_MAX_KEYLEN]; + char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN]; public: bool success = true; From 5a14f36e25777fac4aa4f4e63ff53786575819b5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 14:00:10 +1000 Subject: [PATCH 107/191] * ST7735 color/offset fix for Heltec Tracker V1 --- src/helpers/ui/ST7735Display.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index cdd15aeb0..905ff5037 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -255,6 +255,10 @@ static const uint8_t PROGMEM 0x00, 0x00, // XSTART = 0 0x00, 0x9F }, // XEND = 159 + Rcmd2invert[] = { // Tracker V1, part 2 + 1, // 1 command in list: + ST77XX_INVON, 0 }, // 1: Display is inverted + Rcmd3[] = { // 7735R init, part 3 (red or green tab) 2, // 2 commands in list: ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: @@ -447,8 +451,13 @@ bool ST7735Display::begin() { _height = 80; _width = 160; +#if defined(HELTEC_LORA_V3) // Tracker v1 + _colstart = 26; + _rowstart = 1; +#else _colstart = 24; _rowstart = 0; +#endif _resetAndInit(); @@ -474,6 +483,8 @@ void ST7735Display::_resetAndInit() { displayInit(Rcmd2green160x80); //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR //display.sendCommand(ST77XX_MADCTL, &madctl, 1); +#elif defined(HELTEC_LORA_V3) // Tracker v1 + displayInit(Rcmd2invert); // invert RGB #endif displayInit(Rcmd3); setRotation(DISPLAY_ROTATION); From a99f4a01600d7c03341d34ad1789173dd7230e93 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 15 Jul 2026 16:05:37 +1200 Subject: [PATCH 108/191] increase thinknode m7 max neighbours to 50 --- variants/thinknode_m7/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index ea421b07d..af5d8ea01 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -44,7 +44,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=8 + -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = From 05d4bd2f7f363f2d69a776ed758838328eea2fa3 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 15 Jul 2026 17:45:24 +0800 Subject: [PATCH 109/191] Add a rotary encoder --- examples/companion_radio/ui-new/UITask.cpp | 10 ++ src/helpers/ui/RotaryInput.h | 18 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 12 +- variants/heltec_rc32/HeltecRC32Board.h | 11 +- .../heltec_rc32/HeltecRC32RotaryInput.cpp | 122 ++++++++++++++++++ variants/heltec_rc32/HeltecRC32RotaryInput.h | 26 ++++ variants/heltec_rc32/platformio.ini | 5 +- variants/heltec_rc32/target.cpp | 5 +- variants/heltec_rc32/target.h | 6 + variants/heltec_rc32/variant.h | 3 - 10 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 src/helpers/ui/RotaryInput.h create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.cpp create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.h diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1a..64e61bac2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -750,6 +750,16 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #endif +#if defined(UI_HAS_ROTARY_INPUT) + if (c == 0) { + RotaryInputEvent ev = rotary_input.poll(); + if (ev == RotaryInputEvent::Next) { + c = checkDisplayOn(KEY_NEXT); + } else if (ev == RotaryInputEvent::Prev) { + c = checkDisplayOn(KEY_PREV); + } + } +#endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { int ev = analog_btn.check(); diff --git a/src/helpers/ui/RotaryInput.h b/src/helpers/ui/RotaryInput.h new file mode 100644 index 000000000..2dda695be --- /dev/null +++ b/src/helpers/ui/RotaryInput.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum class RotaryInputEvent : uint8_t { + None, + Next, + Prev, +}; + +class RotaryInput { +public: + virtual ~RotaryInput() = default; + + virtual bool begin() = 0; + virtual RotaryInputEvent poll() = 0; + virtual bool isReady() const = 0; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 37188e9ef..11be483e9 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,4 +1,14 @@ #include "HeltecRC32Board.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif + +#if defined(UI_HAS_ROTARY_INPUT) +RotaryInput& HeltecRC32Board::rotaryInput() { + static HeltecRC32RotaryInput input(&periph_power); + return input; +} +#endif void HeltecRC32Board::begin() { ESP32Board::begin(); @@ -17,7 +27,7 @@ void HeltecRC32Board::begin() { #endif periph_power.begin(); - vext_power.begin(); + periph_power.claim(); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 256cb964c..5f3a0adfc 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,6 +4,9 @@ #include #include #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -15,13 +18,13 @@ protected: public: RefCountedDigitalPin periph_power; - RefCountedDigitalPin vext_power; - HeltecRC32Board() : - periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), - vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotaryInput(); +#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.cpp b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp new file mode 100644 index 000000000..e8b282ff5 --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp @@ -0,0 +1,122 @@ +#include "HeltecRC32RotaryInput.h" + +#include + +namespace { +constexpr uint8_t TCA6408_ADDR = 0x20; +constexpr uint8_t TCA6408_INPUT_REG = 0x00; +constexpr uint8_t TCA6408_POLARITY_REG = 0x02; +constexpr uint8_t TCA6408_CONFIG_REG = 0x03; +constexpr uint8_t TCA6408_ROTARY_A_MASK = 0x01; +constexpr uint8_t TCA6408_ROTARY_B_MASK = 0x02; +constexpr uint8_t TCA6408_ROTARY_MASK = TCA6408_ROTARY_A_MASK | TCA6408_ROTARY_B_MASK; +constexpr uint32_t TCA6408_DEBOUNCE_MS = 5; +} + +bool HeltecRC32RotaryInput::begin() { + initialized = true; + ready = false; + input_state = TCA6408_ROTARY_MASK; + active_low_phase = false; + + if (periph_power && !power_claimed) { + periph_power->claim(); + power_claimed = true; + delay(12); + } + + if (!writeRegister(TCA6408_POLARITY_REG, 0x00) || !writeRegister(TCA6408_CONFIG_REG, 0xFF)) { + return false; + } + + uint8_t state = 0; + if (!readInput(state)) { + return false; + } + + input_state = state & TCA6408_ROTARY_MASK; + ready = true; + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::poll() { + if (!initialized) { + begin(); + return RotaryInputEvent::None; + } + + if (!ready) { + return RotaryInputEvent::None; + } + + uint8_t new_state = 0; + if (!readInput(new_state)) { + return RotaryInputEvent::None; + } + + new_state &= TCA6408_ROTARY_MASK; + RotaryInputEvent event = handleTransition(new_state); + input_state = new_state; + return event; +} + +bool HeltecRC32RotaryInput::writeRegister(uint8_t reg, uint8_t value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +bool HeltecRC32RotaryInput::readInput(uint8_t& value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(TCA6408_INPUT_REG); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(TCA6408_ADDR, static_cast(1)) != 1) { + return false; + } + + value = Wire.read(); + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::handleTransition(uint8_t newState) { + uint8_t changed = (input_state ^ newState) & TCA6408_ROTARY_MASK; + RotaryInputEvent event = RotaryInputEvent::None; + bool a_low = (newState & TCA6408_ROTARY_A_MASK) == 0; + bool b_low = (newState & TCA6408_ROTARY_B_MASK) == 0; + + if (!a_low && !b_low) { + active_low_phase = false; + } + + if (!active_low_phase && (changed & TCA6408_ROTARY_A_MASK) && a_low && !b_low) { + event = RotaryInputEvent::Prev; + active_low_phase = true; + } else if (!active_low_phase && (changed & TCA6408_ROTARY_B_MASK) && b_low && !a_low) { + event = RotaryInputEvent::Next; + active_low_phase = true; + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_A_MASK)) { + bool a_rising = (newState & TCA6408_ROTARY_A_MASK) != 0; + if (a_rising && b_low) { + event = RotaryInputEvent::Prev; + } + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_B_MASK)) { + bool b_rising = (newState & TCA6408_ROTARY_B_MASK) != 0; + if (b_rising && a_low) { + event = RotaryInputEvent::Next; + } + } + + if (event == RotaryInputEvent::None || (millis() - last_event_ms) < TCA6408_DEBOUNCE_MS) { + return RotaryInputEvent::None; + } + + last_event_ms = millis(); + return event; +} diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.h b/variants/heltec_rc32/HeltecRC32RotaryInput.h new file mode 100644 index 000000000..36557dca3 --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class HeltecRC32RotaryInput : public RotaryInput { +public: + explicit HeltecRC32RotaryInput(RefCountedDigitalPin* periphPower = nullptr) : periph_power(periphPower) { } + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return ready; } + +private: + bool writeRegister(uint8_t reg, uint8_t value); + bool readInput(uint8_t& value); + RotaryInputEvent handleTransition(uint8_t newState); + + uint8_t input_state = 0x03; + uint32_t last_event_ms = 0; + bool ready = false; + bool initialized = false; + bool power_claimed = false; + bool active_low_phase = false; + RefCountedDigitalPin* periph_power = nullptr; +}; diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 9284650bd..545472d8c 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -23,8 +23,6 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_BOARD_SDA=21 -D PIN_BOARD_SCL=18 - -D PIN_VEXT_EN=3 - -D PIN_VEXT_EN_ACTIVE=HIGH -D SENSOR_POWER_CTRL_PIN=46 -D SENSOR_POWER_ON=HIGH -D SENSOR_RST_PIN=2 @@ -270,6 +268,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 build_src_filter = ${Heltec_RC32_with_display.build_src_filter} @@ -287,6 +286,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 @@ -308,6 +308,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 6c88d2bb5..386fc5599 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; @@ -26,6 +26,9 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotary_input = board.rotaryInput(); +#endif #endif bool radio_init() { diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h index ae6925980..04cb6d94a 100644 --- a/variants/heltec_rc32/target.h +++ b/variants/heltec_rc32/target.h @@ -10,6 +10,9 @@ #ifdef DISPLAY_CLASS #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifdef HELTEC_RC32_WITH_DISPLAY #include #else @@ -25,6 +28,9 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; +#if defined(UI_HAS_ROTARY_INPUT) + extern RotaryInput& rotary_input; +#endif #endif bool radio_init(); diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index d510ab9b6..0546fbc4d 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -22,9 +22,6 @@ #define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define VEXT_ENABLE 3 -#define VEXT_ON_VALUE HIGH - #define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 From 3469e924586151f2209dc5a2d5a353e8959e0081 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 11:37:12 +0800 Subject: [PATCH 110/191] Enable patch reception --- variants/heltec_rc32/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 545472d8c..d535fb77c 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -55,6 +55,7 @@ build_flags = -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_rc32> + From a472cd7956cb5cb31f650cdb2f4f04d4451ea259 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 15:14:17 +0800 Subject: [PATCH 111/191] Delete duplicate pin definitions --- variants/heltec_rc32/variant.h | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index 0546fbc4d..5f7b8d819 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -3,26 +3,11 @@ #define BUTTON_PIN 0 -#define HAS_GPS 1 -#undef GPS_RX_PIN -#undef GPS_TX_PIN -#define GPS_RX_PIN 44 -#define GPS_TX_PIN 43 -#define PIN_GPS_EN 45 -#define GPS_EN_ACTIVE HIGH -#define PIN_GPS_RESET 40 -#define GPS_RESET_MODE LOW -#define PIN_GPS_PPS 41 - #define I2C_SCL 18 #define I2C_SDA 21 #define SENSOR_INT_PIN 42 -#define SENSOR_RST_PIN 2 -#define SENSOR_POWER_CTRL_PIN 46 -#define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 #define LORA_MOSI 12 @@ -35,14 +20,10 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY 1 #define SX126X_RESET LORA_RESET -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define BATTERY_PIN 7 #define ADC_CHANNEL ADC_CHANNEL_6 #define ADC_CTRL 15 -#define ADC_CTRL_ENABLED HIGH -#define ADC_MULTIPLIER 4.9 #define ADC_ATTENUATION ADC_ATTEN_DB_2_5 #endif From 61a8821d3c6f950738c3bd35bd13b0dfbefc6af0 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 17:45:00 +0800 Subject: [PATCH 112/191] Optimize battery voltage reading --- variants/heltec_rc32/HeltecRC32Board.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 11be483e9..0517f5ff7 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -55,6 +55,7 @@ void HeltecRC32Board::onAfterTransmit() { uint16_t HeltecRC32Board::getBattMilliVolts() { analogReadResolution(12); + analogSetAttenuation(ADC_2_5db); digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); delay(10); uint32_t raw = 0; @@ -62,6 +63,7 @@ uint16_t HeltecRC32Board::getBattMilliVolts() { raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); return (adc_mult * raw); } From 874f0c9ff8ea72db540ac1e1e8a7164b2ff7c49f Mon Sep 17 00:00:00 2001 From: Nick Dunklee Date: Thu, 16 Jul 2026 21:44:35 -0600 Subject: [PATCH 113/191] Minor ifdef rename as it now covers more than just Ed25519 for hardware encryption. Now `USE_CC310_HW_CRYPTO` --- src/Identity.cpp | 4 ++-- src/Utils.cpp | 14 +++++++------- src/helpers/radiolib/RadioLibWrappers.h | 4 ++-- variants/heltec_t096/platformio.ini | 2 +- variants/rak3401/platformio.ini | 2 +- variants/rak4631/platformio.ini | 2 +- variants/t1000-e/platformio.ini | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index 2ab1cefaa..25419fd4f 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -4,7 +4,7 @@ #include #include -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO #include #include "nrf_cc310/include/crys_ec_edw_api.h" #endif @@ -20,7 +20,7 @@ Identity::Identity(const char* pub_hex) { } bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO // nRF52840 CryptoCell CC310 hardware Ed25519 verification. The software // implementations need ~3KB of stack (which can overflow the Adafruit core's // 4KB loop task stack from the advert receive path); the hardware path diff --git a/src/Utils.cpp b/src/Utils.cpp index 9cd44ce24..d4bc8c450 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -2,7 +2,7 @@ #include #include -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO #include #include "nrf_cc310/include/crys_hash.h" #include "nrf_cc310/include/crys_hmac.h" @@ -22,7 +22,7 @@ uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) { } void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO static CRYS_HASH_Result_t result; nRFCrypto.begin(); CRYS_HASH(CRYS_HASH_SHA256_mode, (uint8_t*)msg, (size_t)msg_len, result); @@ -36,7 +36,7 @@ void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_l } void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len) { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO static CRYS_HASHUserContext_t ctx; static CRYS_HASH_Result_t result; nRFCrypto.begin(); @@ -55,7 +55,7 @@ void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int fra } int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO static SaSiAesUserContext_t ctx; SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE }; uint8_t* dp = dest; @@ -89,7 +89,7 @@ int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s } int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO static SaSiAesUserContext_t ctx; SaSiAesUserKeyData_t keyData = { (uint8_t*)shared_secret, CIPHER_KEY_SIZE }; uint8_t* dp = dest; @@ -135,7 +135,7 @@ int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len); -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO static CRYS_HMACUserContext_t hmac_ctx; static CRYS_HASH_Result_t hmac_result; nRFCrypto.begin(); @@ -158,7 +158,7 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes uint8_t hmac[CIPHER_MAC_SIZE]; -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO { static CRYS_HMACUserContext_t hmac_ctx; static CRYS_HASH_Result_t hmac_result; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index f390199f7..4cbbb055d 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,7 +3,7 @@ #include #include -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO #include #endif @@ -84,7 +84,7 @@ public: RadioNoiseListener(PhysicalLayer& radio): _radio(&radio) { } void random(uint8_t* dest, size_t sz) override { -#ifdef USE_CC310_ED25519 +#ifdef USE_CC310_HW_CRYPTO // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. nRFCrypto.begin(); nRFCrypto.Random.generate(dest, (uint16_t)sz); diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index fae7d6424..2f440e837 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -29,7 +29,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D USE_CC310_ED25519=1 + -D USE_CC310_HW_CRYPTO=1 -D PIN_VEXT_EN=26 ; Vext is connected to VDD which is also connected to TFT & GPS -D PIN_VEXT_EN_ACTIVE=HIGH -D PIN_GPS_RX=25 diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 9537fc7f2..ea53b2dc5 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -13,7 +13,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM - -D USE_CC310_ED25519=1 + -D USE_CC310_HW_CRYPTO=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index f6b634ade..f94f7e302 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -22,7 +22,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D USE_CC310_ED25519=1 + -D USE_CC310_HW_CRYPTO=1 -D ENV_INCLUDE_RAK12035=1 -UENV_INCLUDE_BME680 -D ENV_INCLUDE_BME680_BSEC=1 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 9218c5787..d5e3bb535 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -18,7 +18,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D RF_SWITCH_TABLE -D RX_BOOSTED_GAIN=true - -D USE_CC310_ED25519=1 + -D USE_CC310_HW_CRYPTO=1 -D P_LORA_BUSY=7 ; P0.7 -D P_LORA_SCLK=11 ; P0.11 -D P_LORA_NSS=12 ; P0.12 From f515716032e61adbfa4e55178caac5478e9aa89c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 16:05:10 +1000 Subject: [PATCH 114/191] * new NodePrefs for companion --- examples/companion_radio/DataStore.cpp | 70 +++++++----------- examples/companion_radio/DataStore.h | 6 +- examples/companion_radio/MyMesh.cpp | 13 ++-- examples/companion_radio/MyMesh.h | 6 +- examples/companion_radio/NodePrefs.h | 99 +++++++++++++++++++++++++- src/helpers/ConfigSerializer.cpp | 17 +++++ src/helpers/ConfigSerializer.h | 1 + 7 files changed, 155 insertions(+), 57 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 0f3a0f9c5..06c56a7a4 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -189,17 +189,22 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) { return identity_store.save("_main", identity); } -void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) { - if (_fs->exists("/new_prefs")) { - loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename - } else if (_fs->exists("/node_prefs")) { - loadPrefsInt("/node_prefs", prefs, node_lat, node_lon); - savePrefs(prefs, node_lat, node_lon); // save to new filename - _fs->remove("/node_prefs"); // remove old +void DataStore::loadPrefs(NodePrefs& prefs) { + if (_fs->exists("/prefs.json")) { + File file = openRead(_fs, "/prefs.json"); + if (file) { + prefs.loadSerial(file); // new Serial prefs + file.close(); + } + } else if (_fs->exists("/new_prefs")) { + loadPrefsInt("/new_prefs", prefs); + if (savePrefs(prefs) ) { // save to new format + //_fs->remove("/new_prefs"); // remove old + } } } -void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { +void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs) { File file = openRead(_fs, filename); if (file) { uint8_t pad[8]; @@ -207,12 +212,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 file.read(pad, 4); // 36 - file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40 - file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.read((uint8_t *)&_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 + file.read((uint8_t *)&_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 + file.read((uint8_t *)&_prefs._client_repeat, sizeof(_prefs._client_repeat)); // 62 file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 @@ -234,48 +239,21 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no 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 + // migrate old fields + _prefs.setRepeatEn(_prefs._client_repeat != 0); + file.close(); } } -void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { - File file = openWrite(_fs, "/new_prefs"); +bool DataStore::savePrefs(NodePrefs& _prefs) { + File file = openWrite(_fs, "/prefs.json"); if (file) { - uint8_t pad[8]; - memset(pad, 0, sizeof(pad)); - - file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 - file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 - file.write(pad, 4); // 36 - file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40 - file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48 - file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 - file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 - file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 - file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 - file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 - file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 - file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 - file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 - file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 - file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 - file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 - file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 - file.write(pad, 1); // 79 - file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 - file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 - file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 - file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 - 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.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 - file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - + bool success = _prefs.saveSerial(file); file.close(); + return success; } + return false; } void DataStore::loadContacts(DataStoreHost* host) { diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index af5ee7af8..e0a145caf 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -19,7 +19,7 @@ class DataStore { mesh::RTCClock* _clock; IdentityStore identity_store; - void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); + void loadPrefsInt(const char *filename, NodePrefs& prefs); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) void checkAdvBlobFile(); #endif @@ -33,8 +33,8 @@ public: FILESYSTEM* getSecondaryFS() const { return _fsExtra; } bool loadMainIdentity(mesh::LocalIdentity &identity); bool saveMainIdentity(const mesh::LocalIdentity &identity); - void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon); - void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); + void loadPrefs(NodePrefs& prefs); + bool savePrefs(NodePrefs& prefs); void loadContacts(DataStoreHost* host); void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); void loadChannels(DataStoreHost* host); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a78fd29a2..1423d1ffc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -483,7 +483,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { } bool MyMesh::allowPacketForward(const mesh::Packet* packet) { - return _prefs.client_repeat != 0; + return _prefs.isRepeatEn(); } void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { @@ -876,7 +876,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe send_unscoped = false; // defaults - memset(&_prefs, 0, sizeof(_prefs)); + //memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; @@ -887,6 +887,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default //_prefs.rx_delay_base = 10.0f; enable once new algo fixed + _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) #ifdef SX126X_RX_BOOSTED_GAIN _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; @@ -931,7 +932,9 @@ void MyMesh::begin(bool has_display) { #endif // load persisted prefs - _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + _store->loadPrefs(_prefs); + sensors.node_lat = _prefs.node_lat; + sensors.node_lon = _prefs.node_lon; // sanitise bad pref values _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); @@ -1031,7 +1034,7 @@ void MyMesh::handleCmdFrame(size_t len) { i += 40; StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); i += 20; - out_frame[i++] = _prefs.client_repeat; // v9+ + out_frame[i++] = _prefs.isRepeatEn() ? 1 : 0; // v9+ out_frame[i++] = _prefs.path_hash_mode; // v10+ _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_APP_START && @@ -1393,7 +1396,7 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.cr = cr; _prefs.freq = (float)freq / 1000.0; _prefs.bw = (float)bw / 1000.0; - _prefs.client_repeat = repeat; + _prefs.setRepeatEn(repeat != 0); savePrefs(); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f4190f30a..d95b073fb 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,7 +165,11 @@ protected: } public: - void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + void savePrefs() { + _prefs.node_lat = sensors.node_lat; + _prefs.node_lon = sensors.node_lon; + _store->savePrefs(_prefs); + } #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381cea..521693f3a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,5 +1,6 @@ #pragma once #include // For uint8_t, uint32_t +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -8,9 +9,11 @@ #define ADVERT_LOC_NONE 0 #define ADVERT_LOC_SHARE 1 -struct NodePrefs { // persisted to file +class NodePrefs : public ConfigSerializer { // persisted to file +public: float airtime_factor; char node_name[32]; + double node_lat, node_lon; float freq; uint8_t sf; uint8_t cr; @@ -29,9 +32,101 @@ 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 client_repeat; + uint8_t _client_repeat; // DEPRECATED -> use repeat.disable_fwd 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) char default_scope_name[31]; uint8_t default_scope_key[16]; + +private: + class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) + NodePrefs* _parent; + protected: + void structure() override { + def("freq", _parent->freq); + def("bw", _parent->bw); + def("sf", _parent->sf); + def("cr", _parent->cr); + //def("cad", _parent->cad_enabled); + //def("int_thr", _parent->interference_threshold); + def("rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->rx_boosted_gain); + def("tx", _parent->tx_power_dbm); + def("af", _parent->airtime_factor); + def("rxdelay", _parent->rx_delay_base); + //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded + //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded + //def("agc_int", _parent->agc_reset_interval); + def("hash_mode", _parent->path_hash_mode); + def("multi_ack", _parent->multi_acks); + } + public: + RadioPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RadioPrefs radio; + + class GPSPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->gps_enabled); // boolean + def("int", _parent->gps_interval); // interval in seconds + def("adv_loc", _parent->advert_loc_policy); + } + public: + GPSPrefs(NodePrefs* parent) : _parent(parent) { } + }; + GPSPrefs gps; + + class RepeatPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + public: + uint8_t disable_fwd = 1; + protected: + void structure() override { + def("disable", disable_fwd); + //def("f_max", flood_max); + //def("f_max_uns", flood_max_unscoped); + //def("f_max_adv", flood_max_advert); + //def("loop", loop_detect); + } + }; + RepeatPrefs repeat; + + class CompanionPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("auto_max", _parent->autoadd_max_hops); // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + def("defs_nm", _parent->default_scope_name, sizeof(_parent->default_scope_name)); + def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key)); + def("pin", _parent->ble_pin); + def("buzz_q", _parent->buzzer_quiet); + def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config + def("man_add", _parent->manual_add_contacts); + def("tel_base", _parent->telemetry_mode_base); + def("tel_loc", _parent->telemetry_mode_loc); + def("tel_env", _parent->telemetry_mode_env); + } + public: + CompanionPrefs(NodePrefs* parent) : _parent(parent) { } + }; + CompanionPrefs companion; + +protected: + void structure() override { + def("name", node_name, sizeof(node_name)); + //def("adv_int", advert_interval); + //def("f_adv_int", flood_advert_interval); + def("lat", node_lat); + def("lon", node_lon); + def("radio", radio); + def("gps", gps); + def("repeat", repeat); + def("comp", companion); + } +public: + NodePrefs() : radio(this), gps(this), companion(this) { } + // new accessor methods + bool isRepeatEn() const { return repeat.disable_fwd == 0; } + void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } }; \ No newline at end of file diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 766733099..775ef2202 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -140,6 +140,23 @@ void ConfigSerializer::writeComma() { } } +#include + +void ConfigSerializer::def(const char* key, void* value, size_t len) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":\""); + mesh::Utils::printHex(*_context->file(), (uint8_t*) value, len); + _context->file()->print("\""); + } else { + if (_context->keyMatch(_depth, key)) { + memset(value, 0, len); + mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken()); + } + } +} + void ConfigSerializer::def(const char* key, char* value, size_t max_len) { if (_context->op() == OP::WRITE) { writeComma(); diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 5f0dc4888..7e6d6f2a6 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -48,6 +48,7 @@ protected: ConfigSerializer() { } void def(const char* key, char* value, size_t max_len); // max_len inclusive of null + void def(const char* key, void* value, size_t len); // binary blob (encoded in hex) void def(const char* key, int32_t& value); void def(const char* key, int16_t& value); void def(const char* key, int8_t& value); From b26f53a263c41b1587065a5cfcb9049de3999184 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:35:07 +0800 Subject: [PATCH 115/191] Avoid waking display from rotary input --- examples/companion_radio/ui-new/UITask.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 64e61bac2..5a23324cf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,12 +751,12 @@ void UITask::loop() { } #endif #if defined(UI_HAS_ROTARY_INPUT) - if (c == 0) { - RotaryInputEvent ev = rotary_input.poll(); - if (ev == RotaryInputEvent::Next) { - c = checkDisplayOn(KEY_NEXT); - } else if (ev == RotaryInputEvent::Prev) { - c = checkDisplayOn(KEY_PREV); + RotaryInputEvent rotaryEv = rotary_input.poll(); + if (c == 0 && _display != NULL && _display->isOn()) { + if (rotaryEv == RotaryInputEvent::Next) { + c = KEY_NEXT; + } else if (rotaryEv == RotaryInputEvent::Prev) { + c = KEY_PREV; } } #endif From fd7b35f457a580c1e43724135b68a0c1b6d801ba Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:50:25 +0800 Subject: [PATCH 116/191] Move the rotation input to target.cpp --- variants/heltec_rc32/HeltecRC32Board.cpp | 10 ---------- variants/heltec_rc32/HeltecRC32Board.h | 6 ------ variants/heltec_rc32/target.cpp | 6 +++++- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 0517f5ff7..59920d238 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,14 +1,4 @@ #include "HeltecRC32Board.h" -#if defined(UI_HAS_ROTARY_INPUT) -#include "HeltecRC32RotaryInput.h" -#endif - -#if defined(UI_HAS_ROTARY_INPUT) -RotaryInput& HeltecRC32Board::rotaryInput() { - static HeltecRC32RotaryInput input(&periph_power); - return input; -} -#endif void HeltecRC32Board::begin() { ESP32Board::begin(); diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 5f3a0adfc..5b2093c8f 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,9 +4,6 @@ #include #include #include -#if defined(UI_HAS_ROTARY_INPUT) -#include -#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -22,9 +19,6 @@ public: HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); -#if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotaryInput(); -#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 386fc5599..cc7e4deb7 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -1,5 +1,8 @@ #include #include "target.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif HeltecRC32Board board; @@ -27,7 +30,8 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotary_input = board.rotaryInput(); + static HeltecRC32RotaryInput rotaryInputImpl(&board.periph_power); + RotaryInput& rotary_input = rotaryInputImpl; #endif #endif From ae2667ee9eedcb4321d8a45d1f6d8ccbb843375c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 20:55:30 +1000 Subject: [PATCH 117/191] * added unit tests for ConfigSerializer --- platformio.ini | 1 + src/helpers/ConfigSerializer.cpp | 4 +- test/mocks/Arduino.h | 3 + test/mocks/Stream.h | 67 ++++++++- .../test_config_serializer.cpp | 136 ++++++++++++++++++ 5 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 test/mocks/Arduino.h create mode 100644 test/test_config_serializer/test_config_serializer.cpp diff --git a/platformio.ini b/platformio.ini index f3ada1338..17f6c7734 100644 --- a/platformio.ini +++ b/platformio.ini @@ -167,5 +167,6 @@ build_src_filter = -<*> +<../src/Utils.cpp> +<../src/Packet.cpp> + +<../src/helpers/ConfigSerializer.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 775ef2202..9d1323e43 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -111,7 +111,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { if (sp < CONFIG_MAX_DEPTH - 1) { sp++; } else { - Serial.printf("Error: max nesting reached"); // TODO: debug logging + //Serial.printf("Error: max nesting reached"); // TODO: debug logging context.success = false; break; } @@ -119,7 +119,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { if (sp > 0) { sp--; } else { - Serial.printf("Error: too many closing '}'"); // TODO: debug logging + //Serial.printf("Error: too many closing '}'"); // TODO: debug logging context.success = false; break; } diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 000000000..f0821bf4f --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a30297..9b41c54d3 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,71 @@ #pragma once +#include +#include +#include + // Mock Stream class for native testing // Provides minimal interface needed by Utils.h -class Stream { +#define DEC 10 +#define HEX 16 +#define OCT 8 +#define BIN 2 + +class Print +{ public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual size_t write(uint8_t b) { return 1; } + size_t write(const char *str) + { + if(str == NULL) { + return 0; + } + return write((const uint8_t *) str, strlen(str)); + } + virtual size_t write(const uint8_t *buffer, size_t size) { + size_t t = 0; + for (int i = 0; i < size; i++) { t += write(buffer[i]); } + return t; + } + size_t write(const char *buffer, size_t size) + { + return write((const uint8_t *) buffer, size); + } + + virtual size_t print(unsigned char b, int r = DEC) { return 0; } + virtual size_t print(int v, int r = DEC) { return 0; } + virtual size_t print(unsigned int v, int r = DEC) { return 0; } + virtual size_t print(long v, int r = DEC) { return 0; } + virtual size_t print(unsigned long v, int r = DEC) { return 0; } + virtual size_t print(long long v, int r = DEC) { return 0; } + virtual size_t print(unsigned long long v, int r = DEC) { return 0; } + virtual size_t print(double v, int p = 2) { return 0; } + + size_t print(char c) { return write(c); } + size_t print(const char* str) { return write(str); } + + //size_t println(void) { return 0; } + + virtual void flush() { /* Empty implementation for backward compatibility */ } }; + +class Stream: public Print +{ +public: + virtual int available() { return 0; } + virtual int read() { return -1; } + virtual int peek() { return 0; } + + virtual size_t readBytes(char *buffer, size_t length) { + size_t i = 0; + while (i < length && available()) { + buffer[i++] = read(); + } + return i; + } + virtual size_t readBytes(uint8_t *buffer, size_t length) + { + return readBytes((char *) buffer, length); + } +}; \ No newline at end of file diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp new file mode 100644 index 000000000..d6ce68135 --- /dev/null +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -0,0 +1,136 @@ +#include +#include "helpers/ConfigSerializer.h" + +#define TEST_INT_S "56" +#define TEST_INT 56 +#define TEST_FLOAT_S "-6.123" +#define TEST_FLOAT -6.1230f +#define TEST_DOUBLE_S "12.123456" +#define TEST_DOUBLE 12.123456 + +class MockInputStream : public Stream { + const char* _text; + int pos, len; +public: + MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); } + int available() override { return len - pos; } + int read() override { if (pos < len) { return _text[pos++]; } return -1; } + int peek() override { if (pos < len) { return _text[pos]; } return -1; } +}; + +class MockPrintStream : public Stream { + int len = 0; + uint8_t _buf[1024]; +public: + size_t write(uint8_t b) override { + if (len < sizeof(_buf)) { + _buf[len++] = b; + return 1; + } + return 0; + } + + size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(double v, int p = 2) override { + if (p == 6) return Print::print(TEST_DOUBLE_S); + if (p == 4) return Print::print(TEST_FLOAT_S); + return 0; + } + + int getLength() const { return len; } + const uint8_t* getBytes() const { return _buf; } +}; + +class TestStruct : public ConfigSerializer { + protected: + void structure() override { + def("age", age); + def("flags", flags); + def("name", name, sizeof(name)); + } + public: + int32_t age; + char name[16]; + uint8_t flags; +}; + +// ── saveSerial: basic ─────────────────────────────────────────────────────── + +TEST(ConfigSerializer, SaveSerial_Basic) { + MockPrintStream s; + TestStruct data; + + data.age = TEST_INT; + data.flags = TEST_INT; + strcpy(data.name, "Scott"); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"; + EXPECT_EQ(strlen(expect), l); + + bool match = memcmp(s.getBytes(), expect, l) == 0; + EXPECT_TRUE(match); +} + +// ── loadSerial: basic ─────────────────────────────────────────────────────── + +TEST(ConfigSerializer, LoadSerial_Basic) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(TEST_INT, data.flags); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + +TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\""); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_FALSE(success); +} + +TEST(ConfigSerializer, LoadSerial_MissingCommas) { + MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_FALSE(success); +} + +TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { + MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}"); + TestStruct data; + data.flags = 1; + + // should ignore the 'xxx' property + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(1, data.flags); // flags should be unmodified + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + + +// ── main ─────────────────────────────────────────────────────── + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 975a673efd4fcabc21becb8f92f434b432f43265 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:15:09 +1000 Subject: [PATCH 118/191] add calcMaxPacketMillis --- src/helpers/radiolib/RadioLibWrappers.cpp | 18 ++++++++++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5e72336c0..7146e47b6 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -228,3 +228,21 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty)); } + +PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) { + // based on RadioLib's calculateTimeOnAir() + uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10); + uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd + + // preamble + syncword + sfd + header + uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4; + + // airtime for max packet at current radio settings + uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT); + // airtime for payload only (no preamble, header or SOF) + uint32_t payload_us = total_us > preamble_us ? total_us - preamble_us : 4000 - preamble_us; // fallback to 4 secs at worst case + // rescale payload_us for max possible CR + if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } + + return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; +} \ No newline at end of file diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3091832f1..e7fef9104 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,11 @@ #include #include +struct PacketMillis { + uint32_t preambleMillis; // preamble-detect -> header-valid deadline + uint32_t payloadMillis; // header-valid -> rx-done deadline +}; + class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; @@ -47,6 +52,7 @@ public: 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)); } + PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols); virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } From d9f1e12b21dfbac92b90ea7b67dd51a704966d8f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 21:23:40 +1000 Subject: [PATCH 119/191] * loadSerial(), support for \n and \r --- src/helpers/ConfigSerializer.cpp | 2 + .../test_config_serializer.cpp | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 9d1323e43..adff147f4 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -76,6 +76,8 @@ int ConfigSerializer::Context::readNext() { return TOK_ERROR; case EXPECT_STRING_ESCAPE: + if ((c == 'n') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\n'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + if ((c == 'r') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\r'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } return TOK_ERROR; // unsupport escape diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index d6ce68135..c63ea8e82 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -81,6 +81,25 @@ TEST(ConfigSerializer, SaveSerial_Basic) { EXPECT_TRUE(match); } +TEST(ConfigSerializer, SaveSerial_EscChars) { + MockPrintStream s; + TestStruct data; + + data.age = TEST_INT; + data.flags = TEST_INT; + strcpy(data.name, "\"Scott\"\n"); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"; + EXPECT_EQ(strlen(expect), l); + + bool match = memcmp(s.getBytes(), expect, l) == 0; + EXPECT_TRUE(match); +} + // ── loadSerial: basic ─────────────────────────────────────────────────────── TEST(ConfigSerializer, LoadSerial_Basic) { @@ -96,6 +115,30 @@ TEST(ConfigSerializer, LoadSerial_Basic) { EXPECT_TRUE(match); } +TEST(ConfigSerializer, LoadSerial_HandleWhitespace) { + MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } "); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(TEST_INT, data.flags); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + +TEST(ConfigSerializer, LoadSerial_EscChars) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + bool match = strcmp("\"Scott\"\n", data.name) == 0; + EXPECT_TRUE(match); +} + TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) { MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\""); TestStruct data; From 79ef74ea8def0195ddbe340441fec7ce5c943051 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:33:57 +1000 Subject: [PATCH 120/191] add IRQ timeout logic for SX1262 --- src/helpers/radiolib/CustomSX1262.h | 51 +++++++++++++++++++--- src/helpers/radiolib/CustomSX1262Wrapper.h | 1 + 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ca62fc26a..284b79c02 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -3,10 +3,12 @@ #include #include "MeshCore.h" -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSX1262 : public SX1262 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSX1262(Module *mod) : SX1262(mod) { } @@ -99,9 +101,46 @@ class CustomSX1262 : public SX1262 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 1d103f579..45af4cb4b 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,6 +18,7 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); } bool isReceivingPacket() override { From 84d1e24338138327394a447b7faad27934e4c314 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 07:36:39 -0400 Subject: [PATCH 121/191] nrf52_variants: override shutdownPeripherals instead of powerOff for TechoLite, TInpulse and TechoBoard (protect the gate) --- variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.cpp | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.h | 2 +- variants/lilygo_techo_lite/TechoBoard.h | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 5fafcad86..30bbc3a5c 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -47,9 +47,9 @@ public: return "LilyGo T-Impulse-Plus"; } - void powerOff() override { + void shutdownPeripherals() override { // power off system - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); // turn off 3.3v digitalWrite(RT9080_EN, LOW); diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 8143587d5..8a3a54b16 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -87,11 +87,11 @@ void TechoCardBoard::turnOffLeds() { } } -void TechoCardBoard::powerOff() { +void TechoCardBoard::shutdownPeripherals() { nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); turnOffLeds(); digitalWrite(PIN_PWR_EN, LOW); - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); } #endif diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 8a2913a63..a2ee3ea1e 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -28,7 +28,7 @@ public: return "LilyGo T-Echo Card"; } - void powerOff() override; + void shutdownPeripherals() override; void toggleTorch(); void turnOffLeds(); diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index f4c16016b..1e5651e78 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -21,8 +21,8 @@ public: return "LilyGo T-Echo Lite"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED From 0bd871cde6e3256c7de7518c703bb3827d3f9fa2 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:45:31 +1000 Subject: [PATCH 122/191] add IRQ timeout logic for LR11X0 --- src/helpers/radiolib/CustomLR1110.h | 47 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1110Wrapper.h | 2 + 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1a..db0f2af3d 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -4,6 +4,10 @@ #include "MeshCore.h" class CustomLR1110 : public LR1110 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: @@ -32,9 +36,46 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index c6b1acb47..981faf092 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,6 +14,8 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From c644720ea7f969d45731e7cd018f1b57d9186a08 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 08:29:31 -0400 Subject: [PATCH 123/191] uitask: screen and radio poweroff moved to board --- examples/companion_radio/ui-new/UITask.cpp | 3 --- examples/companion_radio/ui-orig/UITask.cpp | 2 -- examples/companion_radio/ui-tiny/UITask.cpp | 2 -- examples/simple_repeater/UITask.cpp | 2 -- 4 files changed, 9 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1a..403ea463f 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -697,9 +697,6 @@ 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 34a7342bb..b48f64121 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -307,8 +307,6 @@ 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 a6cbe9de0..452c02d41 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -566,8 +566,6 @@ 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/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 17c708e31..6751aad69 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -146,8 +146,6 @@ void UITask::loop() { digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff #endif if (millis() > _powering_off_at) { - _display->turnOff(); - radio_driver.powerOff(); _board->powerOff(); // should not return } } From 2189f2294cabbe0213cab8f506a625808f562cd4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 18 Jul 2026 19:22:53 +1000 Subject: [PATCH 124/191] * added prefs initialisers to match original zeroed state --- examples/companion_radio/MyMesh.cpp | 1 - examples/companion_radio/NodePrefs.h | 52 +++++++++--------- src/helpers/CommonCLI.h | 80 +++++++++++++++------------- 3 files changed, 71 insertions(+), 62 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1423d1ffc..b8661cafc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -876,7 +876,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe send_unscoped = false; // defaults - //memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 521693f3a..0c03bd78b 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -11,30 +11,30 @@ class NodePrefs : public ConfigSerializer { // persisted to file public: - float airtime_factor; + float airtime_factor = 0; char node_name[32]; - double node_lat, node_lon; - float freq; - uint8_t sf; - uint8_t cr; - uint8_t multi_acks; - uint8_t manual_add_contacts; - float bw; - int8_t tx_power_dbm; - uint8_t telemetry_mode_base; - uint8_t telemetry_mode_loc; - uint8_t telemetry_mode_env; - float rx_delay_base; - uint32_t ble_pin; - uint8_t advert_loc_policy; - uint8_t buzzer_quiet; - uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) - 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 _client_repeat; // DEPRECATED -> use repeat.disable_fwd - 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) + double node_lat = 0, node_lon = 0; + float freq = 0; + uint8_t sf = 0; + uint8_t cr = 0; + uint8_t multi_acks = 0; + uint8_t manual_add_contacts = 0; + float bw = 0; + int8_t tx_power_dbm = 0; + uint8_t telemetry_mode_base = 0; + uint8_t telemetry_mode_loc = 0; + uint8_t telemetry_mode_env = 0; + float rx_delay_base = 0; + uint32_t ble_pin = 0; + uint8_t advert_loc_policy = 0; + uint8_t buzzer_quiet = 0; + uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled) + uint32_t gps_interval = 0; // GPS read interval in seconds + uint8_t autoadd_config = 0; // bitmask for auto-add contacts config + uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) + uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd + uint8_t path_hash_mode = 0; // which path mode to use when sending + uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -125,7 +125,11 @@ protected: def("comp", companion); } public: - NodePrefs() : radio(this), gps(this), companion(this) { } + NodePrefs() : radio(this), gps(this), companion(this) { + node_name[0] = 0; + default_scope_name[0] = 0; + memset(default_scope_key, 0, sizeof(default_scope_key)); + } // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index aab33d114..69fe03150 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -23,51 +23,51 @@ class NodePrefs : public ConfigSerializer { public: // in-memory backing data - float airtime_factor; + float airtime_factor = 0; char node_name[32]; - double node_lat, node_lon; + double node_lat = 0, node_lon = 0; char password[16]; - float freq; - int8_t tx_power_dbm; - uint8_t disable_fwd; - uint8_t advert_interval; // minutes / 2 - uint8_t flood_advert_interval; // hours - float rx_delay_base; - float tx_delay_factor; + float freq = 0; + int8_t tx_power_dbm = 0; + uint8_t disable_fwd = 0; + uint8_t advert_interval = 0; // minutes / 2 + uint8_t flood_advert_interval = 0; // hours + float rx_delay_base = 0; + float tx_delay_factor = 0; char guest_password[16]; - float direct_tx_delay_factor; + float direct_tx_delay_factor = 0; uint32_t guard; - uint8_t sf; - uint8_t cr; - uint8_t allow_read_only; - uint8_t multi_acks; - float bw; - uint8_t flood_max; - uint8_t flood_max_unscoped; - uint8_t flood_max_advert; - uint8_t interference_threshold; - uint8_t agc_reset_interval; // secs / 4 + uint8_t sf = 0; + uint8_t cr = 0; + uint8_t allow_read_only = 0; + uint8_t multi_acks = 0; + float bw = 0; + uint8_t flood_max = 0; + uint8_t flood_max_unscoped = 0; + uint8_t flood_max_advert = 0; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // secs / 4 // Bridge settings - uint8_t bridge_enabled; // boolean - uint16_t bridge_delay; // milliseconds (default 500 ms) - uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx) - uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200) - uint8_t bridge_channel; // 1-14 (ESP-NOW only) + uint8_t bridge_enabled = 0; // boolean + uint16_t bridge_delay = 0; // milliseconds (default 500 ms) + uint8_t bridge_pkt_src = 0; // 0 = logTx, 1 = logRx (default logTx) + uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200) + uint8_t bridge_channel = 0; // 1-14 (ESP-NOW only) char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting - uint8_t powersaving_enabled; // boolean + uint8_t powersaving_enabled = 0; // boolean // Gps settings - uint8_t gps_enabled; - uint32_t gps_interval; // in seconds - uint8_t advert_loc_policy; - uint32_t discovery_mod_timestamp; - float adc_multiplier; + uint8_t gps_enabled = 0; + uint32_t gps_interval = 0; // in seconds + uint8_t advert_loc_policy = 0; + uint32_t discovery_mod_timestamp = 0; + float adc_multiplier = 0; 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; - uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t rx_boosted_gain = 0; // power settings + uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting + uint8_t path_hash_mode = 0; // which path mode to use when sending + uint8_t loop_detect = 0; + uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) private: class RadioPrefs : public ConfigSerializer { @@ -182,7 +182,13 @@ protected: } public: - NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { } + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { + node_name[0] = 0; + password[0] = 0; + guest_password[0] = 0; + bridge_secret[0] = 0; + owner_info[0] = 0; + } }; class CommonCLICallbacks { From a0b52be4adde46df63bec79aee1e69eb567b51ef Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 18 Jul 2026 20:12:21 +1000 Subject: [PATCH 125/191] * RP2040 build fixes * new prefs file now "/prefs.json" --- examples/companion_radio/NodePrefs.h | 4 ++-- platformio.ini | 2 +- src/helpers/CommonCLI.cpp | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 0c03bd78b..39a5386a9 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -65,7 +65,7 @@ private: }; RadioPrefs radio; - class GPSPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + class GPSPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) NodePrefs* _parent; protected: void structure() override { @@ -78,7 +78,7 @@ private: }; GPSPrefs gps; - class RepeatPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + class RepeatPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) public: uint8_t disable_fwd = 1; protected: diff --git a/platformio.ini b/platformio.ini index 17f6c7734..fd0afcdf4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -100,7 +100,7 @@ lib_deps = extends = arduino_base upload_protocol = picotool board_build.core = earlephilhower -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#4e22a0d ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9 build_flags = ${arduino_base.build_flags} -D RP2040_PLATFORM diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 56a46d467..9d0571d98 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -28,8 +28,12 @@ static bool isValidName(const char *n) { } void CommonCLI::loadPrefs(FILESYSTEM* fs) { - if (fs->exists("/ser_prefs")) { - File file = fs->open("/ser_prefs"); + if (fs->exists("/prefs.json")) { +#if defined(RP2040_PLATFORM) + File file = fs->open("/prefs.json", "r"); +#else + File file = fs->open("/prefs.json"); +#endif if (file) { _prefs->loadSerial(file); // new Serial prefs file.close(); @@ -137,12 +141,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy bool CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/ser_prefs"); - File file = fs->open("/ser_prefs", FILE_O_WRITE); + fs->remove("/prefs.json"); + File file = fs->open("/prefs.json", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) - File file = fs->open("/ser_prefs", "w"); + File file = fs->open("/prefs.json", "w"); #else - File file = fs->open("/ser_prefs", "w", true); + File file = fs->open("/prefs.json", "w", true); #endif if (file) { bool success = _prefs->saveSerial(file); From fb2c61f862fcd4c6e08cf0f882175ca260052b13 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Mon, 22 Jun 2026 16:12:00 +0200 Subject: [PATCH 126/191] fix: prevent ESP32 KISS modem stalls under USB backpressure Switch ESP32-S3 KISS modem environments to HWCDC and move outbound KISS writes to a non-blocking queued frame path so loop() and TX completion keep progressing when the host reads slowly. Add native backpressure regression tests and correct the native_kiss_modem test filter so this suite runs directly with pio test. --- docs/kiss_modem_protocol.md | 14 +- examples/kiss_modem/KissModem.cpp | 199 ++++++++++-- examples/kiss_modem/KissModem.h | 36 ++- examples/kiss_modem/main.cpp | 10 +- platformio.ini | 15 + test/mocks/Arduino.h | 17 + test/mocks/CayenneLPP.h | 11 + test/mocks/Identity.h | 39 +++ test/mocks/Mesh.h | 27 ++ test/mocks/Stream.h | 21 +- test/mocks/Utils.h | 44 +++ test/mocks/helpers/SensorManager.h | 10 + test/test_kiss_modem/test_tx_backpressure.cpp | 294 ++++++++++++++++++ variants/heltec_v4/platformio.ini | 5 + variants/station_g2/platformio.ini | 5 + 15 files changed, 701 insertions(+), 46 deletions(-) create mode 100644 test/mocks/Arduino.h create mode 100644 test/mocks/CayenneLPP.h create mode 100644 test/mocks/Identity.h create mode 100644 test/mocks/Mesh.h create mode 100644 test/mocks/Utils.h create mode 100644 test/mocks/helpers/SensorManager.h create mode 100644 test/test_kiss_modem/test_tx_backpressure.cpp diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 9a9962248..3f4dbf9c4 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -41,7 +41,7 @@ Maximum unescaped frame size: 512 bytes. | Command | Value | Data | Description | |-------------|--------|--------------------|-------------------------------------------------------------| -| Data | `0x00` | Raw packet | Queue packet for transmission | +| Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) | | TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) | | Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) | | SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) | @@ -58,6 +58,12 @@ Maximum unescaped frame size: 512 bytes. Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. +Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07). + +### Host Output Backpressure + +Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses. + ### CSMA Behavior The TNC implements p-persistent CSMA for half-duplex operation: @@ -156,15 +162,15 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic | MacFailed | `0x04` | MAC verification failed | | UnknownCmd | `0x05` | Unknown sub-command | | EncryptFailed | `0x06` | Encryption failed | -| TxBusy | `0x07` | Transmit busy | +| TxBusy | `0x07` | Radio TX busy, or host output queue full | ### Unsolicited Events The TNC sends these SetHardware frames without a preceding request: -**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. +**TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped. -**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame. +**RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame. ## Data Formats diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501d..7dcfc8d7d 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _getStatsCallback = nullptr; _config = {0, 0, 0, 0, 0}; _signal_report_enabled = true; + resetOutputQueue(); } void KissModem::begin() { @@ -30,37 +31,168 @@ void KissModem::begin() { _rx_active = false; _has_pending_tx = false; _tx_state = TX_IDLE; + resetOutputQueue(); } -void KissModem::writeByte(uint8_t b) { - if (b == KISS_FEND) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFEND); - } else if (b == KISS_FESC) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFESC); - } else { - _serial.write(b); +void KissModem::resetOutputQueue() { + _tx_frame_head = 0; + _tx_frame_tail = 0; + _tx_frame_count = 0; + _tx_busy_error_pending = false; + _tx_done_pending = false; + _tx_done_result = 0; +} + +void KissModem::popTxFrame() { + _tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count--; +} + +uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) { + if (b == KISS_FEND || b == KISS_FESC) { + if (idx + 2 > max_len) { + return 0; + } + dest[idx++] = KISS_FESC; + dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC; + return idx; } + if (idx + 1 > max_len) { + return 0; + } + dest[idx++] = b; + return idx; } -void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(type); +uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) { + if (max_len < KISS_FRAME_BOUNDARY_BYTES) { + return 0; + } + + uint16_t idx = 0; + dest[idx++] = KISS_FEND; + + idx = appendEscapedByte(dest, idx, max_len, type); + if (idx == 0) { + return 0; + } + for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); + idx = appendEscapedByte(dest, idx, max_len, data[i]); + if (idx == 0) { + return 0; + } } - _serial.write(KISS_FEND); + + if (idx + 1 > max_len) { + return 0; + } + dest[idx++] = KISS_FEND; + return idx; } -void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(KISS_CMD_SETHARDWARE); - writeByte(sub_cmd); - for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); +bool KissModem::tryFlushFrames() { + while (_tx_frame_count > 0) { + const uint8_t idx = _tx_frame_head; + const uint16_t frame_len = _tx_frame_len[idx]; + uint16_t written_len = _tx_frame_written[idx]; + + if (written_len >= frame_len) { + popTxFrame(); + continue; + } + + const int available = _serial.availableForWrite(); + if (available <= 0) { + return false; + } + + const uint16_t remaining = frame_len - written_len; + const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining; + if (chunk_len == 0) { + return false; + } + + size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len); + if (chunk_written == 0) { + return false; + } + + written_len += (uint16_t)chunk_written; + _tx_frame_written[idx] = written_len; + + if (written_len < frame_len) { + return false; + } + + popTxFrame(); } - _serial.write(KISS_FEND); + return true; +} + +bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) { + if (mark_busy_error) { + _tx_busy_error_pending = true; + } + return false; + } + const uint8_t idx = _tx_frame_tail; + uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx])); + if (frame_len == 0) { + return false; + } + + _tx_frame_len[idx] = frame_len; + _tx_frame_written[idx] = 0; + _tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count++; + tryFlushFrames(); + return true; +} + +bool KissModem::queuePendingBusyError() { + if (!_tx_busy_error_pending) { + return true; + } + const uint8_t err = HW_ERR_TX_BUSY; + if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) { + return false; + } + _tx_busy_error_pending = false; + return true; +} + +bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (len > KISS_MAX_FRAME_SIZE) { + return false; + } + _tx_hw_payload[0] = sub_cmd; + if (len > 0) { + memcpy(_tx_hw_payload + 1, data, len); + } + return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error); +} + +bool KissModem::queuePendingTxDone() { + if (!_tx_done_pending) { + return true; + } + if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) { + return false; + } + _tx_done_pending = false; + return true; +} + +void KissModem::setTxDonePending(uint8_t result) { + _tx_done_result = result; + _tx_done_pending = true; + _tx_state = TX_DONE_PENDING; +} + +bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { + return queueHardwareFrame(sub_cmd, data, len, true); } void KissModem::writeHardwareError(uint8_t error_code) { @@ -68,6 +200,8 @@ void KissModem::writeHardwareError(uint8_t error_code) { } void KissModem::loop() { + tryFlushFrames(); + while (_serial.available()) { uint8_t b = _serial.read(); @@ -106,6 +240,8 @@ void KissModem::loop() { } processTx(); + tryFlushFrames(); + queuePendingBusyError(); } void KissModem::processFrame() { @@ -295,10 +431,7 @@ void KissModem::processTx() { _tx_timer = millis(); _tx_state = TX_SENDING; } else { - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x00); } } break; @@ -306,14 +439,15 @@ void KissModem::processTx() { case TX_SENDING: if (_radio.isSendComplete()) { _radio.onSendFinished(); - uint8_t result = 0x01; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x01); } else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) { _radio.onSendFinished(); - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); + setTxDonePending(0x00); + } + break; + + case TX_DONE_PENDING: + if (queuePendingTxDone()) { _has_pending_tx = false; _tx_state = TX_IDLE; } @@ -322,8 +456,7 @@ void KissModem::processTx() { } void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { - writeFrame(KISS_CMD_DATA, packet, len); - if (_signal_report_enabled) { + if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) { uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; writeHardwareFrame(HW_RESP_RX_META, meta, 2); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6de..a23e459be 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -13,6 +13,14 @@ #define KISS_MAX_FRAME_SIZE 512 #define KISS_MAX_PACKET_SIZE 255 +#define KISS_FRAME_BOUNDARY_BYTES 2 +#define KISS_TYPE_BYTES 1 +#define KISS_HW_SUBCMD_BYTES 1 +#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES) +#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES) +#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE) +#define KISS_TX_FRAME_QUEUE_DEPTH 2 +#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES) #define KISS_CMD_DATA 0x00 #define KISS_CMD_TXDELAY 0x01 @@ -94,7 +102,8 @@ enum TxState { TX_WAIT_CLEAR, TX_SLOT_WAIT, TX_DELAY, - TX_SENDING + TX_SENDING, + TX_DONE_PENDING }; class KissModem { @@ -130,10 +139,28 @@ class KissModem { RadioConfig _config; bool _signal_report_enabled; + uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE]; + uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH]; + uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH]; + uint8_t _tx_frame_head; + uint8_t _tx_frame_tail; + uint8_t _tx_frame_count; + bool _tx_busy_error_pending; + bool _tx_done_pending; + uint8_t _tx_done_result; + uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE]; - void writeByte(uint8_t b); - void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); - void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); + static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b); + static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len); + void resetOutputQueue(); + void popTxFrame(); + bool tryFlushFrames(); + bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true); + bool queuePendingBusyError(); + bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error); + bool queuePendingTxDone(); + void setTxDonePending(uint8_t result); + bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); void writeHardwareError(uint8_t error_code); void processFrame(); void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len); @@ -182,4 +209,5 @@ public: bool isTxBusy() const { return _tx_state != TX_IDLE; } /** True only when radio is actually transmitting; use to skip recvRaw in main loop. */ bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; } + bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 7fbcaed12..95a06ee63 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -20,6 +20,8 @@ #define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 #define AGC_RESET_INTERVAL_MS 30000 +#define USB_TX_TIMEOUT_MS 50 +#define USB_TX_BUFFER_SIZE 1024 StdRNG rng; mesh::LocalIdentity identity; @@ -111,6 +113,12 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); +#if defined(ESP32) + Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); +#if ARDUINO_USB_MODE + Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); +#endif +#endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif @@ -126,7 +134,7 @@ void setup() { void loop() { modem->loop(); - if (!modem->isActuallyTransmitting()) { + if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) { if (!modem->isTxBusy()) { if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { radio_driver.resetAGC(); diff --git a/platformio.ini b/platformio.ini index f3ada1338..2de71a37f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -169,3 +169,18 @@ build_src_filter = +<../src/Packet.cpp> lib_deps = google/googletest @ 1.17.0 + +[env:native_kiss_modem] +platform = native +test_framework = googletest +build_flags = -std=c++17 + -I test/mocks + -I src + -I examples/kiss_modem +test_build_src = yes +test_filter = test_kiss_modem +build_src_filter = + -<*> + +<../examples/kiss_modem/KissModem.cpp> +lib_deps = + google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 000000000..77499fe41 --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "Stream.h" + +inline uint32_t g_mock_millis = 0; + +using std::isnan; + +inline uint32_t millis() { + return g_mock_millis; +} + +inline void delay(uint32_t ms) { + g_mock_millis += ms; +} diff --git a/test/mocks/CayenneLPP.h b/test/mocks/CayenneLPP.h new file mode 100644 index 000000000..9d51c0fcf --- /dev/null +++ b/test/mocks/CayenneLPP.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class CayenneLPP { +public: + explicit CayenneLPP(size_t) {} + const uint8_t* getBuffer() const { return nullptr; } + uint16_t getSize() const { return 0; } +}; diff --git a/test/mocks/Identity.h b/test/mocks/Identity.h new file mode 100644 index 000000000..5c3d25e94 --- /dev/null +++ b/test/mocks/Identity.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "Utils.h" + +namespace mesh { + +class Identity { +public: + uint8_t pub_key[PUB_KEY_SIZE]; + + Identity() { + std::memset(pub_key, 0, sizeof(pub_key)); + } + + explicit Identity(const uint8_t* src) { + std::memcpy(pub_key, src, PUB_KEY_SIZE); + } + + bool verify(const uint8_t*, const uint8_t*, int) const { + return true; + } +}; + +class LocalIdentity : public Identity { +public: + LocalIdentity() : Identity() {} + + void sign(uint8_t* sig, const uint8_t*, int) const { + std::memset(sig, 0x5A, SIGNATURE_SIZE); + } + + void calcSharedSecret(uint8_t* secret, const uint8_t*) const { + std::memset(secret, 0x11, PUB_KEY_SIZE); + } +}; + +} diff --git a/test/mocks/Mesh.h b/test/mocks/Mesh.h new file mode 100644 index 000000000..b6c263c19 --- /dev/null +++ b/test/mocks/Mesh.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace mesh { + +class Radio { +public: + virtual ~Radio() = default; + virtual bool isReceiving() { return false; } + virtual uint32_t getEstAirtimeFor(uint16_t) { return 10; } + virtual bool startSendRaw(const uint8_t*, uint16_t) { return true; } + virtual bool isSendComplete() { return true; } + virtual void onSendFinished() {} + virtual int16_t getNoiseFloor() { return -120; } +}; + +class MainBoard { +public: + virtual ~MainBoard() = default; + virtual uint16_t getBattMilliVolts() { return 4200; } + virtual float getMCUTemperature() { return 25.0f; } + virtual const char* getManufacturerName() { return "mock-board"; } + virtual void reboot() {} +}; + +} diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a30297..cf33785d1 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,23 @@ #pragma once -// Mock Stream class for native testing -// Provides minimal interface needed by Utils.h +#include +#include class Stream { public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual ~Stream() = default; + virtual size_t write(uint8_t) { return 1; } + virtual size_t write(const uint8_t* buffer, size_t size) { + size_t total = 0; + for (size_t i = 0; i < size; i++) { + total += write(buffer[i]); + } + return total; + } + virtual int available() { return 0; } + virtual int availableForWrite() { return 0; } + virtual int read() { return -1; } + virtual void flush() {} + virtual void print(char) {} + virtual void print(const char*) {} }; diff --git a/test/mocks/Utils.h b/test/mocks/Utils.h new file mode 100644 index 000000000..9bb6b060b --- /dev/null +++ b/test/mocks/Utils.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#define PUB_KEY_SIZE 32 +#define PRV_KEY_SIZE 64 +#define SIGNATURE_SIZE 64 +#define CIPHER_MAC_SIZE 16 + +namespace mesh { + +class RNG { +public: + virtual ~RNG() = default; + virtual void random(uint8_t* dest, size_t sz) = 0; +}; + +class Utils { +public: + static void sha256(uint8_t* hash, size_t hash_len, const uint8_t*, int) { + std::memset(hash, 0, hash_len); + } + + static int encryptThenMAC(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + int out_len = src_len + CIPHER_MAC_SIZE; + std::memset(dest, 0xAA, CIPHER_MAC_SIZE); + std::memcpy(dest + CIPHER_MAC_SIZE, src, src_len); + return out_len; + } + + static int MACThenDecrypt(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + if (src_len < CIPHER_MAC_SIZE) { + return 0; + } + int out_len = src_len - CIPHER_MAC_SIZE; + std::memcpy(dest, src + CIPHER_MAC_SIZE, out_len); + return out_len; + } +}; + +} diff --git a/test/mocks/helpers/SensorManager.h b/test/mocks/helpers/SensorManager.h new file mode 100644 index 000000000..d1a41cb5d --- /dev/null +++ b/test/mocks/helpers/SensorManager.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include "CayenneLPP.h" + +class SensorManager { +public: + virtual ~SensorManager() = default; + virtual bool querySensors(uint8_t, CayenneLPP&) { return false; } +}; diff --git a/test/test_kiss_modem/test_tx_backpressure.cpp b/test/test_kiss_modem/test_tx_backpressure.cpp new file mode 100644 index 000000000..39a5e3b74 --- /dev/null +++ b/test/test_kiss_modem/test_tx_backpressure.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "KissModem.h" + +static constexpr int TEST_TX_AVAILABLE_BYTES = 4096; +static constexpr size_t TEST_DEFAULT_MAX_WRITE_CHUNK = SIZE_MAX; +static constexpr size_t TEST_PARTIAL_WRITE_CHUNK = 2; +static constexpr int TEST_PARTIAL_WRITE_FLUSH_LOOPS = 3; +static constexpr uint8_t TEST_SNR = 8; +static constexpr uint8_t TEST_RSSI = 200; + +class BlockingStream : public Stream { +public: + void pushRx(const std::vector& bytes) { + std::lock_guard lock(_mutex); + for (uint8_t b : bytes) { + _rx.push(b); + } + } + + void setBlockWrites(bool blocked) { + { + std::lock_guard lock(_mutex); + _block_writes = blocked; + } + _cv.notify_all(); + } + + bool isWriteBlocked() const { + return _entered_block.load(); + } + + size_t writesCount() const { + std::lock_guard lock(_mutex); + return _writes.size(); + } + + std::vector writesSnapshot() const { + std::lock_guard lock(_mutex); + return _writes; + } + + int availableForWrite() override { + std::lock_guard lock(_mutex); + return _block_writes ? 0 : TEST_TX_AVAILABLE_BYTES; + } + + void setMaxWriteChunk(size_t chunk) { + std::lock_guard lock(_mutex); + _max_write_chunk = chunk; + } + + size_t write(const uint8_t* buffer, size_t size) override { + std::unique_lock lock(_mutex); + while (_block_writes) { + _entered_block.store(true); + _cv.wait(lock); + } + const size_t chunk = (size < _max_write_chunk) ? size : _max_write_chunk; + for (size_t i = 0; i < chunk; i++) { + _writes.push_back(buffer[i]); + } + return chunk; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + int available() override { + std::lock_guard lock(_mutex); + return static_cast(_rx.size()); + } + + int read() override { + std::lock_guard lock(_mutex); + if (_rx.empty()) { + return -1; + } + int b = _rx.front(); + _rx.pop(); + return b; + } + +private: + mutable std::mutex _mutex; + std::condition_variable _cv; + std::queue _rx; + std::vector _writes; + bool _block_writes = false; + std::atomic _entered_block = false; + size_t _max_write_chunk = TEST_DEFAULT_MAX_WRITE_CHUNK; +}; + +class FakeRNG : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + for (size_t i = 0; i < sz; i++) { + dest[i] = 0; + } + } +}; + +class FakeRadio : public mesh::Radio { +public: + bool isReceiving() override { return false; } + uint32_t getEstAirtimeFor(uint16_t) override { return 10; } + bool startSendRaw(const uint8_t*, uint16_t) override { + _start_send_count++; + return _start_send_result; + } + bool isSendComplete() override { return _send_complete; } + void onSendFinished() override { _send_finished_count++; } + int16_t getNoiseFloor() override { return -120; } + + void setStartSendResult(bool result) { _start_send_result = result; } + void setSendComplete(bool complete) { _send_complete = complete; } + int startSendCount() const { return _start_send_count; } + int sendFinishedCount() const { return _send_finished_count; } + +private: + bool _start_send_result = true; + bool _send_complete = true; + int _start_send_count = 0; + int _send_finished_count = 0; +}; + +class FakeBoard : public mesh::MainBoard { +public: + uint16_t getBattMilliVolts() override { return 4200; } + float getMCUTemperature() override { return 24.0f; } + const char* getManufacturerName() override { return "test-board"; } + void reboot() override {} +}; + +class FakeSensors : public SensorManager { +public: + bool querySensors(uint8_t, CayenneLPP&) override { return false; } +}; + +class KissModemFixture : public ::testing::Test { +protected: + BlockingStream serial; + mesh::LocalIdentity identity; + FakeRNG rng; + FakeRadio radio; + FakeBoard board; + FakeSensors sensors; + KissModem modem; + + KissModemFixture() + : modem(serial, identity, rng, radio, board, sensors) { + modem.begin(); + } + + static std::vector dataFrame(const std::vector& packet) { + std::vector frame = {KISS_FEND, KISS_CMD_DATA}; + frame.insert(frame.end(), packet.begin(), packet.end()); + frame.push_back(KISS_FEND); + return frame; + } + + void advanceToTxSending() { + modem.loop(); + modem.loop(); + delay((uint32_t)KISS_DEFAULT_TXDELAY * 10); + modem.loop(); + } +}; + +TEST_F(KissModemFixture, PingResponseShouldNotStallLoopUnderTxBackpressure) { + serial.setBlockWrites(true); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + + auto future = std::async(std::launch::async, [this]() { + modem.loop(); + }); + + auto status = future.wait_for(std::chrono::milliseconds(100)); + EXPECT_EQ(status, std::future_status::ready) << "KissModem::loop blocked in serial write under TX backpressure"; + EXPECT_FALSE(serial.isWriteBlocked()) << "KissModem entered blocking write path"; + + serial.setBlockWrites(false); + future.wait(); + modem.loop(); + EXPECT_GT(serial.writesCount(), 0U) << "KissModem did not flush queued response after backpressure cleared"; +} + +TEST_F(KissModemFixture, PingResponseKeepsStandardKissFraming) { + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PingResponseKeepsFramingWithPartialBulkWrites) { + serial.setMaxWriteChunk(TEST_PARTIAL_WRITE_CHUNK); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + for (int i = 0; i < TEST_PARTIAL_WRITE_FLUSH_LOOPS; i++) { + modem.loop(); + } + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PacketAndMetaAreQueuedTogetherUnderBackpressure) { + static constexpr uint8_t TEST_PACKET[] = {0x01, 0x02, 0x03}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + serial.setBlockWrites(false); + modem.loop(); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET[0], TEST_PACKET[1], TEST_PACKET[2], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, RadioTxCompletionAdvancesWhileHostOutputIsBackedUp) { + serial.pushRx(dataFrame({0x42})); + advanceToTxSending(); + ASSERT_EQ(radio.startSendCount(), 1); + + serial.setBlockWrites(true); + modem.loop(); + EXPECT_EQ(radio.sendFinishedCount(), 1); + EXPECT_TRUE(modem.isTxBusy()); + + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_TX_DONE, 0x01, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); + EXPECT_FALSE(modem.isTxBusy()); +} + +TEST_F(KissModemFixture, QueueFullReportsBusyWithoutDroppingQueuedFrames) { + static constexpr uint8_t TEST_PACKET_ONE[] = {0x11, 0x12}; + static constexpr uint8_t TEST_PACKET_TWO[] = {0x21, 0x22}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_ONE, sizeof(TEST_PACKET_ONE)); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_TWO, sizeof(TEST_PACKET_TWO)); + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET_ONE[0], TEST_PACKET_ONE[1], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_TX_BUSY, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, QueuedEncoderEscapesKissSpecialBytes) { + static constexpr uint8_t TEST_PACKET[] = {KISS_FEND, KISS_FESC, 0x01}; + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC, 0x01, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, MaxPacketWorstCaseEscapingFitsQueuedFrame) { + std::vector packet(KISS_MAX_PACKET_SIZE, KISS_FEND); + std::vector expected = {KISS_FEND, KISS_CMD_DATA}; + for (size_t i = 0; i < packet.size(); i++) { + expected.push_back(KISS_FESC); + expected.push_back(KISS_TFEND); + } + expected.push_back(KISS_FEND); + expected.insert(expected.end(), {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}); + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, packet.data(), packet.size()); + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fabf38272..cb76beb3d 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -432,5 +432,10 @@ lib_deps = [env:heltec_v4_kiss_modem] extends = Heltec_lora32_v4 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Heltec_lora32_v4.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Heltec_lora32_v4.build_src_filter} +<../examples/kiss_modem/> diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 6432b5238..508ffe4b5 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -241,5 +241,10 @@ lib_deps = [env:Station_G2_kiss_modem] extends = Station_G2 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Station_G2.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Station_G2.build_src_filter} +<../examples/kiss_modem/> From 39ff5b879acadb124ee535fcf2386a83cfff08e0 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Tue, 23 Jun 2026 15:15:13 +0200 Subject: [PATCH 127/191] fix: run KISS modem unit tests in CI via native_kiss_modem env The new test_kiss_modem suite needs KissModem headers and sources that only native_kiss_modem provides. Scope native to test_utils and run both envs in the unit test workflow. --- .github/workflows/run-unit-tests.yml | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e3af3aafb..5d48f4c67 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/setup-build-environment - name: Run Unit Tests - run: pio test -e native -vv + run: pio test -e native -e native_kiss_modem -vv - name: Upload Test Results # Upload test results even if the test step failed. diff --git a/platformio.ini b/platformio.ini index 2de71a37f..cdd67d277 100644 --- a/platformio.ini +++ b/platformio.ini @@ -163,6 +163,7 @@ build_flags = -std=c++17 -I src -I test/mocks test_build_src = yes +test_filter = test_utils build_src_filter = -<*> +<../src/Utils.cpp> From 3b69e7873bd76b52c3d3469d7496ff64a9048db5 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 26 Jun 2026 00:11:33 +0200 Subject: [PATCH 128/191] fix: guard HWCDC serial config behind ARDUINO_USB_MODE setTxTimeoutMs() and setTxBufferSize() are HWCDC-only APIs; calling them on HardwareSerial (UART-bridge boards like T-Beam) fails to compile. Co-Authored-By: Adam Gessaman --- examples/kiss_modem/main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 95a06ee63..5836a6941 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,11 +113,9 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) +#if defined(ESP32) && ARDUINO_USB_MODE Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); -#if ARDUINO_USB_MODE Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); -#endif #endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif From b28bf00348c1cf555a88a5988bd0e889958a2517 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 19 Jul 2026 22:06:36 +1000 Subject: [PATCH 129/191] fix: setMaxPacketMillis() should return void --- src/helpers/radiolib/CustomLR1110.h | 2 +- src/helpers/radiolib/CustomSX1262.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index db0f2af3d..87a7005f5 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -72,7 +72,7 @@ class CustomLR1110 : public LR1110 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 284b79c02..18932488a 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,7 +137,7 @@ class CustomSX1262 : public SX1262 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; From aee53a8c3e3176eb9e26fd781774f198d4bb6f30 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:59:12 +1000 Subject: [PATCH 130/191] NRF52Board - ShutdownPeripherals() housekeeping Improvements: - Add condition to only turn off display if on (avoids wedging the shutdown on some displays) - Reset LoRa radio before calling radio_driver.powerOff() to ensure it is in a known good state (and will accept the command) - Ensure SPI is started so LoRa radio commands can be sent (some shutdowns can happen before this) - Add gate for P_LORA_NSS pin defined before writing to the pin --- src/helpers/NRF52Board.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5fb1e55ed..23f7cafc2 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -314,14 +314,38 @@ float NRF52Board::getMCUTemperature() { void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS - display.turnOff(); + if (display.isOn()) { + display.turnOff(); + } #endif - + // Prep LoRa radio for power down + #ifdef P_LORA_RESET + digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us) + delayMicroseconds(200); + digitalWrite(P_LORA_RESET, HIGH); + #endif + #if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI) + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio + #endif + #ifdef P_LORA_BUSY + pinMode(P_LORA_BUSY, INPUT); + uint32_t started_at = millis(); + while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready + #endif + #ifdef P_LORA_NSS + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off LoRa radio_driver.powerOff(); // Keep LoRa inactive during deepsleep - digitalWrite(P_LORA_NSS, HIGH); + #ifdef P_LORA_NSS + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off GPS if any if(sensors.getLocationProvider() != NULL) { From cc221330ba1dbac71d2214dbcdb3699d74b02e83 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sun, 19 Jul 2026 10:17:08 -0700 Subject: [PATCH 131/191] Fix nRF52 builds: guard SerialEthernetInterface with ETHERNET_ENABLED SerialEthernetInterface.cpp is compiled for every nRF52 target because PlatformIO builds all .cpp files under src/. It includes SerialEthernetInterface.h, which unconditionally pulls in -- a library only present in the RAK4631 Ethernet env's lib_deps. As a result any other nRF52 board (e.g. Heltec T114) fails to build with 'RAK13800_W5100S.h: No such file or directory'. Wrap the contents of both files in '#ifdef ETHERNET_ENABLED' so they compile to empty translation units on non-Ethernet builds. RAK4631 Ethernet envs define ETHERNET_ENABLED and are unaffected. Fixes #2985 --- src/helpers/nrf52/SerialEthernetInterface.cpp | 4 ++++ src/helpers/nrf52/SerialEthernetInterface.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 708910232..36a998a4e 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -1,3 +1,5 @@ +#ifdef ETHERNET_ENABLED + #include "SerialEthernetInterface.h" #include "EthernetMac.h" #include @@ -262,3 +264,5 @@ bool SerialEthernetInterface::isConnected() const { void SerialEthernetInterface::loop() { Ethernet.maintain(); } + +#endif // ETHERNET_ENABLED diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 95ce8a521..b8b4e94b2 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -1,5 +1,7 @@ #pragma once +#ifdef ETHERNET_ENABLED + #include "helpers/BaseSerialInterface.h" #include #include @@ -76,3 +78,5 @@ class SerialEthernetInterface : public BaseSerialInterface { #define ETHERNET_DEBUG_PRINTLN(...) {} #define ETHERNET_DEBUG_PRINT_IP(...) {} #endif + +#endif // ETHERNET_ENABLED From ae48072ff059c53c94803cd9e6583c763a13a703 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:40:49 +1000 Subject: [PATCH 132/191] SX1262: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomSX1262.h | 11 +++++++---- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 18932488a..103f51672 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,10 +137,13 @@ class CustomSX1262 : public SX1262 { return false; } - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 45af4cb4b..bfea50ec9 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,7 +18,9 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From 121b72712b361eaa5685f8c90144e5fccad5690d Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:42:22 +1000 Subject: [PATCH 133/191] LR1110: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomLR1110.h | 13 ++++++++----- src/helpers/radiolib/CustomLR1110Wrapper.h | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 87a7005f5..0470e48f2 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -71,11 +71,14 @@ class CustomLR1110 : public LR1110 { _activityAt = 0; _headerSeen = false; return false; } - - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 981faf092..44230c61c 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,8 +14,9 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); - + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From 91e65af9dd8514627b320f19a442100bafb868d7 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 20:28:38 +1000 Subject: [PATCH 134/191] fix: heltec t1: add missing bodmer TFT_eSPI library --- variants/heltec_t1/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index d5c3caf18..eaa8af378 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 debug_tool = jlink upload_protocol = nrfutil From ab5c45f51612c268b7fa4dca423d5b3dab88df8e Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 21:05:12 +1000 Subject: [PATCH 135/191] fix: clear the correct IRQ flag for timeout on preamble detected --- src/helpers/radiolib/CustomSX1262.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 103f51672..15efed5ca 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -125,7 +125,7 @@ class CustomSX1262 : public SX1262 { if (preamble) { if (_activityAt == 0) _activityAt = now; if (now - _activityAt > _preambleMillis) { - clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); _activityAt = 0; MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); From d2bbe4d8d3629f1d44b27b8af0264799ce54c63e Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:43:25 +1000 Subject: [PATCH 136/191] Fix Xiao nRF52840 repeater compile failure Compile fails due to user_btn not declared in board target. Fixes: - Add MomentaryButton declaration for user_btn in variants/xiao_nrf52/target.cpp - Add momentary button helper include in variants/xiao_nrf52/target.h - Add extern linkage for MomentaryButton -> user_btn in variants/xiao_nrf52/target.h --- variants/xiao_nrf52/XiaoNrf52Board.h | 2 +- variants/xiao_nrf52/target.cpp | 1 + variants/xiao_nrf52/target.h | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index b2638a440..e71e7f651 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -47,7 +47,7 @@ public: #ifdef PIN_USER_BTN // configure button press to wake up when in powered off state - nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif NRF52Board::powerOff(); diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index ab6fe279a..def1dbbea 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -4,6 +4,7 @@ #ifdef DISPLAY_CLASS DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); #endif XiaoNrf52Board board; diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index bb3d2a81f..20093904a 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -11,7 +11,9 @@ #ifdef DISPLAY_CLASS #include + #include extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; #endif extern XiaoNrf52Board board; From 3885c67c8eaf46ce66e28252338df783ca178a95 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 137/191] fix: synchronize BLE receive queue --- src/helpers/BoundedQueue.h | 39 +++++++++++++ src/helpers/esp32/SerialBLEInterface.cpp | 41 ++++++++----- src/helpers/esp32/SerialBLEInterface.h | 10 ++-- .../test_bounded_queue/test_bounded_queue.cpp | 58 +++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/helpers/BoundedQueue.h create mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h new file mode 100644 index 000000000..07ecb4fff --- /dev/null +++ b/src/helpers/BoundedQueue.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace mesh { + +template +class BoundedQueue { + static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); + + T _items[Capacity]; + size_t _head = 0; + size_t _size = 0; + +public: + bool push(const T& item) { + if (_size == Capacity) return false; + _items[(_head + _size) % Capacity] = item; + _size++; + return true; + } + + bool pop(T& item) { + if (_size == 0) return false; + item = _items[_head]; + _head = (_head + 1) % Capacity; + _size--; + return true; + } + + void clear() { + _head = 0; + _size = 0; + } + + size_t size() const { return _size; } +}; + +} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e3..9c81dc7bc 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -118,17 +118,30 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat if (len > MAX_FRAME_SIZE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); - } else if (recv_queue_len >= FRAME_QUEUE_SIZE) { - BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } else { - recv_queue[recv_queue_len].len = len; - memcpy(recv_queue[recv_queue_len].buf, rxValue, len); - recv_queue_len++; + Frame frame = {}; + frame.len = len; + memcpy(frame.buf, rxValue, len); + + portENTER_CRITICAL(&recv_queue_mux); + bool queued = recv_queue.push(frame); + portEXIT_CRITICAL(&recv_queue_mux); + + if (!queued) { + BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); + } } } // ---------- public methods +void SerialBLEInterface::clearBuffers() { + portENTER_CRITICAL(&recv_queue_mux); + recv_queue.clear(); + portEXIT_CRITICAL(&recv_queue_mux); + send_queue_len = 0; +} + void SerialBLEInterface::enable() { if (_isEnabled) return; @@ -202,17 +215,15 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - if (recv_queue_len > 0) { // check recv queue - size_t len = recv_queue[0].len; // take from top of queue - memcpy(dest, recv_queue[0].buf, len); + Frame frame = {}; + portENTER_CRITICAL(&recv_queue_mux); + bool received = recv_queue.pop(frame); + portEXIT_CRITICAL(&recv_queue_mux); - BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); - - recv_queue_len--; - for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue - recv_queue[i] = recv_queue[i + 1]; - } - return len; + if (received) { + memcpy(dest, frame.buf, frame.len); + BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); + return frame.len; } if (pServer->getConnectedCount() == 0) deviceConnected = false; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd1..e2d600a0e 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,10 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" +#include "../BoundedQueue.h" #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -24,12 +26,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - int recv_queue_len; - Frame recv_queue[FRAME_QUEUE_SIZE]; + portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; + mesh::BoundedQueue recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; - void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void clearBuffers(); protected: // BLESecurityCallbacks methods @@ -58,7 +60,7 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; - send_queue_len = recv_queue_len = 0; + send_queue_len = 0; } /** diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp new file mode 100644 index 000000000..3e2e2de7f --- /dev/null +++ b/test/test_bounded_queue/test_bounded_queue.cpp @@ -0,0 +1,58 @@ +#include + +#include + +TEST(BoundedQueue, PreservesOrderAcrossWraparound) { + mesh::BoundedQueue queue; + int value = 0; + + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); + EXPECT_FALSE(queue.push(4)); + + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(0, value); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(1, value); + + EXPECT_TRUE(queue.push(4)); + EXPECT_TRUE(queue.push(5)); + + for (int expected = 2; expected < 6; expected++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(expected, value); + } + EXPECT_FALSE(queue.pop(value)); +} + +TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { + mesh::BoundedQueue queue; + int value = 0; + + for (int cycle = 0; cycle < 1000; cycle++) { + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); + for (int i = 0; i < 4; i++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(cycle * 4 + i, value); + } + } +} + +TEST(BoundedQueue, ClearDropsPendingItems) { + mesh::BoundedQueue queue; + int value = 0; + + EXPECT_TRUE(queue.push(1)); + EXPECT_TRUE(queue.push(2)); + queue.clear(); + + EXPECT_EQ(0u, queue.size()); + EXPECT_FALSE(queue.pop(value)); + EXPECT_TRUE(queue.push(3)); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(3, value); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 60030469c93c592bddb5f1a4123717484a7db857 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 138/191] fix: enable Heltec V4 USB serial --- boards/heltec_v4.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json index 36cdfc043..0d3a52cd0 100644 --- a/boards/heltec_v4.json +++ b/boards/heltec_v4.json @@ -9,7 +9,7 @@ "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], @@ -40,4 +40,4 @@ }, "url": "https://heltec.org/", "vendor": "heltec" -} \ No newline at end of file +} From 9fd6b419e5d134811e1775e7e3d129df9b83f12f Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 139/191] fix: preserve bonded BLE reconnects --- src/helpers/esp32/SerialBLEInterface.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e3..c1c046021 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -103,10 +103,9 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param void SerialBLEInterface::onDisconnect(BLEServer* pServer) { BLE_DEBUG_PRINTLN("onDisconnect()"); + deviceConnected = false; if (_isEnabled) { adv_restart_time = millis() + ADVERT_RESTART_DELAY; - - // loop() will detect this on next loop, and set deviceConnected to false } } @@ -215,8 +214,6 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { return len; } - if (pServer->getConnectedCount() == 0) deviceConnected = false; - if (deviceConnected != oldDeviceConnected) { if (!deviceConnected) { // disconnecting clearBuffers(); From 79dc1de6fc4a371d48f677a2de574d250fc0d7b2 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 140/191] fix: preserve UTF-8 advert names --- docs/cli_commands.md | 2 +- src/helpers/AdvertDataHelpers.cpp | 14 ++--- src/helpers/UTF8Helpers.h | 56 ++++++++++++++++++++ test/test_utf8_helpers/test_utf8_helpers.cpp | 38 +++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/helpers/UTF8Helpers.h create mode 100644 test/test_utf8_helpers/test_utf8_helpers.cpp diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 5598dba55..b618ae2bf 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -305,7 +305,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** Varies by board -**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte. +**Note:** Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary. --- diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 0e05620ec..998733ae0 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -1,4 +1,5 @@ #include +#include uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) { app_data[0] = _type; @@ -16,11 +17,12 @@ app_data[0] |= ADV_FEAT2_MASK; memcpy(&app_data[i], &_extra2, 2); i += 2; } - if (_name && *_name != 0) { - app_data[0] |= ADV_NAME_MASK; - const char* sp = _name; - while (*sp && i < MAX_ADVERT_DATA_SIZE) { - app_data[i++] = *sp++; + if (_name && *_name != 0) { + size_t name_len = mesh::validUtf8PrefixLength(_name, MAX_ADVERT_DATA_SIZE - i); + if (name_len > 0) { + app_data[0] |= ADV_NAME_MASK; + memcpy(&app_data[i], _name, name_len); + i += name_len; } } return i; @@ -84,4 +86,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_ } } } -} \ No newline at end of file +} diff --git a/src/helpers/UTF8Helpers.h b/src/helpers/UTF8Helpers.h new file mode 100644 index 000000000..e06cf4a62 --- /dev/null +++ b/src/helpers/UTF8Helpers.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace mesh { + +inline bool isUtf8Continuation(uint8_t byte) { + return (byte & 0xC0) == 0x80; +} + +inline size_t validUtf8PrefixLength(const char* text, size_t max_bytes) { + if (text == nullptr) return 0; + + size_t offset = 0; + while (text[offset] != '\0') { + const uint8_t first = static_cast(text[offset]); + size_t sequence_length = 0; + + if (first <= 0x7F) { + sequence_length = 1; + } else if (first >= 0xC2 && first <= 0xDF) { + sequence_length = 2; + } else if (first >= 0xE0 && first <= 0xEF) { + sequence_length = 3; + } else if (first >= 0xF0 && first <= 0xF4) { + sequence_length = 4; + } else { + break; + } + + if (offset + sequence_length > max_bytes) break; + + bool complete = true; + for (size_t i = 1; i < sequence_length; i++) { + if (text[offset + i] == '\0' || !isUtf8Continuation(static_cast(text[offset + i]))) { + complete = false; + break; + } + } + if (!complete) break; + + if (sequence_length == 3) { + const uint8_t second = static_cast(text[offset + 1]); + if ((first == 0xE0 && second < 0xA0) || (first == 0xED && second > 0x9F)) break; + } else if (sequence_length == 4) { + const uint8_t second = static_cast(text[offset + 1]); + if ((first == 0xF0 && second < 0x90) || (first == 0xF4 && second > 0x8F)) break; + } + + offset += sequence_length; + } + return offset; +} + +} // namespace mesh diff --git a/test/test_utf8_helpers/test_utf8_helpers.cpp b/test/test_utf8_helpers/test_utf8_helpers.cpp new file mode 100644 index 000000000..ee9535220 --- /dev/null +++ b/test/test_utf8_helpers/test_utf8_helpers.cpp @@ -0,0 +1,38 @@ +#include + +#include + +TEST(UTF8Helpers, KeepsCompleteNameWithinLimit) { + const char* name = "Example RPT 🔋🇵🇱"; + + EXPECT_EQ(24u, mesh::validUtf8PrefixLength(name, 24)); +} + +TEST(UTF8Helpers, StopsBeforeCodePointCrossingLimit) { + const char* name = "Example RPT 🔋🇵🇱"; + + EXPECT_EQ(20u, mesh::validUtf8PrefixLength(name, 23)); +} + +TEST(UTF8Helpers, RejectsMalformedAndTruncatedSequences) { + const char overlong[] = {'A', static_cast(0xC0), static_cast(0xAF), 0}; + const char surrogate[] = {'A', static_cast(0xED), static_cast(0xA0), static_cast(0x80), 0}; + const char out_of_range[] = {'A', static_cast(0xF4), static_cast(0x90), static_cast(0x80), static_cast(0x80), 0}; + const char truncated[] = {'A', static_cast(0xF0), static_cast(0x9F), 0}; + + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(overlong, sizeof(overlong))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(surrogate, sizeof(surrogate))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(out_of_range, sizeof(out_of_range))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(truncated, sizeof(truncated))); +} + +TEST(UTF8Helpers, RejectsUnexpectedContinuationByte) { + const char invalid[] = {'A', static_cast(0x80), 'B', 0}; + + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(invalid, sizeof(invalid))); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 0de2b4fc5f208489a8523882f050ead033ed3d8b Mon Sep 17 00:00:00 2001 From: =UltimateCodeWarrior Date: Mon, 20 Jul 2026 19:24:38 -0700 Subject: [PATCH 141/191] Move Supreme OLED address to variant config Keep SH1106Display generic by relying on DISPLAY_ADDRESS while the T-Beam Supreme variant declares its 0x3D OLED address. --- src/helpers/ui/SH1106Display.cpp | 15 ++++----------- .../lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 86c6089bc..57ccfc2e4 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,17 +11,10 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - // Address selection: on some board revisions (notably the LilyGo T-Beam - // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a - // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it - // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). - uint8_t addr = 0; - if (i2c_probe(Wire, 0x3D)) { - addr = 0x3D; - } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { - addr = DISPLAY_ADDRESS; - } - return addr && display.begin(addr, true); + // Wire must already be initialised by board.begin() before this is called. + // Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS + // in their variant/platformio configuration. + return i2c_probe(Wire, DISPLAY_ADDRESS) && display.begin(DISPLAY_ADDRESS, true); } void SH1106Display::turnOn() diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 029461565..8bfc4093a 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -18,6 +18,7 @@ build_flags = -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D DISPLAY_CLASS=SH1106Display + -D DISPLAY_ADDRESS=0x3D -D LORA_TX_POWER=22 -D P_LORA_TX_LED=6 -D PIN_BOARD_SDA=17 From 4d4d7c37467ce2ad2255f70e53ea40624834393a Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Tue, 21 Jul 2026 07:17:01 +0100 Subject: [PATCH 142/191] refactor: use FreeRTOS BLE receive queue --- src/helpers/BoundedQueue.h | 39 ------------- src/helpers/esp32/SerialBLEInterface.cpp | 16 +---- src/helpers/esp32/SerialBLEInterface.h | 10 +++- .../test_bounded_queue/test_bounded_queue.cpp | 58 ------------------- 4 files changed, 10 insertions(+), 113 deletions(-) delete mode 100644 src/helpers/BoundedQueue.h delete mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h deleted file mode 100644 index 07ecb4fff..000000000 --- a/src/helpers/BoundedQueue.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -namespace mesh { - -template -class BoundedQueue { - static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); - - T _items[Capacity]; - size_t _head = 0; - size_t _size = 0; - -public: - bool push(const T& item) { - if (_size == Capacity) return false; - _items[(_head + _size) % Capacity] = item; - _size++; - return true; - } - - bool pop(T& item) { - if (_size == 0) return false; - item = _items[_head]; - _head = (_head + 1) % Capacity; - _size--; - return true; - } - - void clear() { - _head = 0; - _size = 0; - } - - size_t size() const { return _size; } -}; - -} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 9c81dc7bc..2dd20b2ff 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -123,11 +123,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat frame.len = len; memcpy(frame.buf, rxValue, len); - portENTER_CRITICAL(&recv_queue_mux); - bool queued = recv_queue.push(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (!queued) { + if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } } @@ -136,9 +132,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat // ---------- public methods void SerialBLEInterface::clearBuffers() { - portENTER_CRITICAL(&recv_queue_mux); - recv_queue.clear(); - portEXIT_CRITICAL(&recv_queue_mux); + xQueueReset(recv_queue); send_queue_len = 0; } @@ -216,11 +210,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } Frame frame = {}; - portENTER_CRITICAL(&recv_queue_mux); - bool received = recv_queue.pop(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (received) { + if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); return frame.len; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index e2d600a0e..9fb6adba9 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,12 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" -#include "../BoundedQueue.h" #include #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -26,8 +26,9 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; - mesh::BoundedQueue recv_queue; + StaticQueue_t recv_queue_state; + uint8_t recv_queue_storage[FRAME_QUEUE_SIZE * sizeof(Frame)]; + QueueHandle_t recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; @@ -60,6 +61,9 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; + recv_queue = xQueueCreateStatic( + FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state + ); send_queue_len = 0; } diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp deleted file mode 100644 index 3e2e2de7f..000000000 --- a/test/test_bounded_queue/test_bounded_queue.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include - -#include - -TEST(BoundedQueue, PreservesOrderAcrossWraparound) { - mesh::BoundedQueue queue; - int value = 0; - - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); - EXPECT_FALSE(queue.push(4)); - - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(0, value); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(1, value); - - EXPECT_TRUE(queue.push(4)); - EXPECT_TRUE(queue.push(5)); - - for (int expected = 2; expected < 6; expected++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(expected, value); - } - EXPECT_FALSE(queue.pop(value)); -} - -TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { - mesh::BoundedQueue queue; - int value = 0; - - for (int cycle = 0; cycle < 1000; cycle++) { - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); - for (int i = 0; i < 4; i++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(cycle * 4 + i, value); - } - } -} - -TEST(BoundedQueue, ClearDropsPendingItems) { - mesh::BoundedQueue queue; - int value = 0; - - EXPECT_TRUE(queue.push(1)); - EXPECT_TRUE(queue.push(2)); - queue.clear(); - - EXPECT_EQ(0u, queue.size()); - EXPECT_FALSE(queue.pop(value)); - EXPECT_TRUE(queue.push(3)); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(3, value); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} From 78240e9477feab86b2ea1d24073ca580f9e59415 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 21 Jul 2026 19:22:09 +1000 Subject: [PATCH 143/191] * optimised: value-init not needed --- src/helpers/esp32/SerialBLEInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 7084b7d9e..6371cf335 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -208,7 +208,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - Frame frame = {}; + Frame frame; if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); From a71cc238546e36c5c31c196eb4ec0cf528746017 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 21 Jul 2026 12:23:20 -0700 Subject: [PATCH 144/191] fix(room_server): support RX boosted gain The room server never supported RX boosted gain, while the repeater does. Three consequences on SX1262/SX1268 boards: - boosted gain was never applied to the radio at boot, so a room server ran in power-saving RX mode while an identical repeater ran boosted - a real receive-sensitivity difference with nothing pointing at it - _prefs.rx_boosted_gain was never initialised (the prefs are memset to 0 before defaults are set), so it defaulted to off - 'set radio.rxgain on' replied "Error: unsupported" but CommonCLI had already written the value to prefs and saved them, so a stale setting survived reboot and was never applied Mirror the repeater's implementation: initialise the pref default under the same USE_SX1262/USE_SX1268 and SX126X_RX_BOOSTED_GAIN guards, apply it during radio init, and override setRxBoostedGain() so the CLI reports success and takes effect. Built Heltec_v3_room_server (SX1262) and LilyGo_T3S3_sx1276_room_server (guards compile out cleanly). --- examples/simple_room_server/MyMesh.cpp | 13 +++++++++++++ examples/simple_room_server/MyMesh.h | 1 + 2 files changed, 14 insertions(+) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e808..b64851285 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -658,6 +658,14 @@ 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; + +#if defined(USE_SX1262) || defined(USE_SX1268) +#ifdef SX126X_RX_BOOSTED_GAIN + _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; +#else + _prefs.rx_boosted_gain = 1; // enabled by default; +#endif +#endif _prefs.radio_fem_rxgain = 1; next_post_idx = 0; @@ -700,6 +708,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); + radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); @@ -807,6 +816,10 @@ void MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); } +bool MyMesh::setRxBoostedGain(bool enable) { + return radio_driver.setRxBoostedGainMode(enable); +} + void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 6bab9dc2d..caef69209 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -206,6 +206,7 @@ public: void dumpLogFile() override; void setTxPower(int8_t power_dbm) override; + bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char *reply) override { strcpy(reply, "not supported"); From b8504e55c6fe000bd4f0f412d04823d3b177e0bd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 16:03:33 +1000 Subject: [PATCH 145/191] * refactor of Color mapping in UITasks / DisplayDrivers * color displays now with a new light theme --- examples/companion_radio/ui-new/UITask.cpp | 67 ++++++++++++------- examples/companion_radio/ui-orig/UITask.cpp | 27 ++++---- .../ui-tiny/ScrollingStatusBar.h | 2 +- examples/companion_radio/ui-tiny/UITask.cpp | 31 +++++---- examples/simple_repeater/UITask.cpp | 12 ++-- examples/simple_room_server/UITask.cpp | 8 +-- examples/simple_sensor/UITask.cpp | 8 +-- src/helpers/ui/DisplayDriver.h | 14 +++- src/helpers/ui/E213Display.cpp | 30 ++++++--- src/helpers/ui/E213Display.h | 5 +- src/helpers/ui/E290Display.cpp | 30 ++++++--- src/helpers/ui/E290Display.h | 5 +- src/helpers/ui/GxEPDDisplay.cpp | 29 +++++--- src/helpers/ui/GxEPDDisplay.h | 4 +- src/helpers/ui/LGFXDisplay.cpp | 48 +++++-------- src/helpers/ui/LGFXDisplay.h | 4 +- src/helpers/ui/NV3001BDisplay.cpp | 34 +++++----- src/helpers/ui/NV3001BDisplay.h | 4 +- src/helpers/ui/NullDisplayDriver.h | 4 +- src/helpers/ui/SH1106Display.cpp | 19 ++++-- src/helpers/ui/SH1106Display.h | 4 +- src/helpers/ui/SSD1306Display.cpp | 19 ++++-- src/helpers/ui/SSD1306Display.h | 4 +- src/helpers/ui/ST7735Display.cpp | 46 +++++-------- src/helpers/ui/ST7735Display.h | 4 +- src/helpers/ui/ST7789Display.cpp | 51 +++++--------- src/helpers/ui/ST7789Display.h | 4 +- src/helpers/ui/ST7789LCDDisplay.cpp | 47 +++++-------- src/helpers/ui/ST7789LCDDisplay.h | 4 +- src/helpers/ui/U8g2Display.cpp | 12 ++++ src/helpers/ui/U8g2Display.h | 16 ++--- variants/lilygo_techo_card/platformio.ini | 2 +- 32 files changed, 313 insertions(+), 285 deletions(-) create mode 100644 src/helpers/ui/U8g2Display.cpp diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a26dc19a2..09903db0b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -53,23 +53,24 @@ public: int render(DisplayDriver& display) override { // meshcore logo - display.setColor(DisplayDriver::BLUE); + display.setColor(UIColor::corp_blue); int logoWidth = 128; display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); uint16_t websiteWidth = display.getTextWidth(website); display.setCursor((display.width() - websiteWidth) / 2, 22); display.print(website); // version info - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 35, _version_info); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE); @@ -128,7 +129,7 @@ class HomeScreen : public UIScreen { int iconHeight = 10; int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner int iconY = 0; - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::title_txt); // battery outline display.drawRect(iconX, iconY, iconWidth, iconHeight); @@ -143,7 +144,7 @@ class HomeScreen : public UIScreen { // show muted icon if buzzer is muted #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); } #endif @@ -188,34 +189,37 @@ public: } int render(DisplayDriver& display) override { + display.setColor(UIColor::title_bkg); + display.fillRect(0, 0, display.width(), 12); char tmp[80]; // node name display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::title_txt); char filtered_name[sizeof(_node_prefs->node_name)]; display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); - display.setCursor(0, 0); + display.setCursor(0, 2); display.print(filtered_name); // battery voltage renderBatteryIndicator(display, _task->getBattMilliVolts()); // curr page indicator + display.setColor(UIColor::title_bkg); int y = 14; int x = display.width() / 2 - 5 * (HomePage::Count-1); for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { if (i == _page) { - display.fillRect(x-1, y-1, 3, 3); + display.fillRect(x-1, y-1, 4, 4); } else { - display.fillRect(x, y, 1, 1); + display.fillRect(x, y, 2, 2); } } if (_page == HomePage::FIRST) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); - display.drawTextCentered(display.width() / 2, 20, tmp); + display.drawTextCentered(display.width() / 2, 22, tmp); #ifdef WIFI_SSID IPAddress ip = WiFi.localIP(); @@ -224,19 +228,19 @@ public: display.drawTextCentered(display.width() / 2, 54, tmp); #endif if (_task->hasConnection()) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 43, "< Connected >"); } else if (the_mesh.getBLEPin() != 0) { // BT pin - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, 43, tmp); } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); int y = 20; for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; @@ -260,7 +264,7 @@ public: display.print(tmp); } } else if (_page == HomePage::RADIO) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(1); // freq / sf display.setCursor(0, 20); @@ -279,15 +283,17 @@ public: sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); } else if (_page == HomePage::BLUETOOTH) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); } else if (_page == HomePage::ADVERT) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); + display.setColor(UIColor::secondary_txt); display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 } else if (_page == HomePage::GPS) { @@ -305,24 +311,33 @@ public: #else strcpy(buf, gps_state ? "gps on" : "gps off"); #endif + display.setColor(UIColor::primary_txt); display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "Can't access GPS"); } else { + display.setColor(UIColor::primary_txt); strcpy(buf, nmea->isValid()?"fix":"no fix"); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "sat"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%d", nmea->satellitesCount()); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "pos"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%.4f %.4f", nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "alt"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%.2f", nmea->getAltitude()/1000.); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; @@ -390,7 +405,9 @@ public: strcpy(name, "unk"); sprintf(buf, ""); } display.setCursor(0, y); + display.setColor(UIColor::secondary_txt); display.print(name); + display.setColor(UIColor::primary_txt); display.setCursor( display.width()-display.getTextWidth(buf)-1, y ); @@ -401,11 +418,13 @@ public: else sensors_scroll_offset = 0; #endif } else if (_page == HomePage::SHUTDOWN) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.setTextSize(1); if (_shutdown_init) { + display.setColor(UIColor::warning_txt); display.drawTextCentered(display.width() / 2, 34, "hibernating..."); } else { + display.setColor(UIColor::secondary_txt); display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } @@ -498,7 +517,7 @@ public: char tmp[16]; display.setCursor(0, 0); display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); sprintf(tmp, "Unread: %d", num_unread); display.print(tmp); @@ -518,13 +537,13 @@ public: display.drawRect(0, 11, display.width(), 1); // horiz line display.setCursor(0, 14); - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::secondary_txt); char filtered_origin[sizeof(p->origin)]; display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin)); display.print(filtered_origin); display.setCursor(0, 25); - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); char filtered_msg[sizeof(p->msg)]; display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg)); display.printWordWrap(filtered_msg, display.width()); @@ -806,9 +825,9 @@ void UITask::loop() { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; - _display->setColor(DisplayDriver::DARK); + _display->setColor(UIColor::popup_bkg); _display->fillRect(p, y, _display->width() - p*2, y); - _display->setColor(DisplayDriver::LIGHT); // draw box border + _display->setColor(UIColor::popup_txt); // draw box border _display->drawRect(p, y, _display->width() - p*2, y); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _next_refresh = _alert_expiry; // will need refresh when alert is dismissed @@ -845,7 +864,7 @@ void UITask::loop() { if (_display != NULL) { _display->startFrame(); _display->setTextSize(2); - _display->setColor(DisplayDriver::RED); + _display->setColor(UIColor::warning_txt); _display->drawTextCentered(_display->width() / 2, 20, "Low Battery."); _display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!"); _display->endFrame(); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b48f64121..09fc8e770 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -167,7 +167,7 @@ void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { int iconHeight = 12; int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner int iconY = 0; - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); // battery outline _display->drawRect(iconX, iconY, iconWidth, iconHeight); @@ -188,7 +188,7 @@ void UITask::renderCurrScreen() { _display->setTextSize(1.4); uint16_t textWidth = _display->getTextWidth(_alert); _display->setCursor((_display->width() - textWidth) / 2, 22); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::warning_txt); _display->print(_alert); _alert[0] = 0; _need_refresh = true; @@ -197,30 +197,29 @@ void UITask::renderCurrScreen() { // render message preview _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); _display->setCursor(0, 12); - _display->setColor(DisplayDriver::YELLOW); + _display->setColor(UIColor::secondary_txt); _display->print(_origin); _display->setCursor(0, 24); - _display->setColor(DisplayDriver::LIGHT); _display->print(_msg); _display->setCursor(_display->width() - 28, 9); _display->setTextSize(2); - _display->setColor(DisplayDriver::ORANGE); + _display->setColor(UIColor::primary_txt); sprintf(tmp, "%d", _msgcount); _display->print(tmp); - _display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114 + _display->setColor(UIColor::secondary_txt); // last color will be kept on T114 } else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // version info - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t textWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - textWidth) / 2, 22); @@ -229,7 +228,7 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // battery voltage @@ -237,7 +236,7 @@ void UITask::renderCurrScreen() { // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); + _display->setColor(UIColor::secondary_txt); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); @@ -248,14 +247,14 @@ void UITask::renderCurrScreen() { // BT pin if (!_connected && the_mesh.getBLEPin() != 0) { - _display->setColor(DisplayDriver::RED); + _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); _display->print(tmp); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); } else { - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); } } _need_refresh = false; diff --git a/examples/companion_radio/ui-tiny/ScrollingStatusBar.h b/examples/companion_radio/ui-tiny/ScrollingStatusBar.h index f5943c9b6..9967489fd 100644 --- a/examples/companion_radio/ui-tiny/ScrollingStatusBar.h +++ b/examples/companion_radio/ui-tiny/ScrollingStatusBar.h @@ -104,7 +104,7 @@ public: if (_status[0] == 0) return; display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); // if (_needs_redraw) { // _text_width = display.getTextWidth(_status); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 452c02d41..5b9ebfd39 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -56,24 +56,24 @@ public: int render(DisplayDriver& display) override { if (millis() < version_after) { // meshcore logo - display.setColor(DisplayDriver::BLUE); + display.setColor(UIColor::corp_blue); int logoWidth = 72; display.drawXbm(0, 0, meshcore_logo, 72, 36); } else { // meshcore website const char* website = "meshcore.io"; - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); uint16_t websiteWidth = display.getTextWidth(website); display.setCursor((display.width() - websiteWidth) / 2, 9); display.print(website); // version info - display.setColor(DisplayDriver::LIGHT); display.setTextSize(1); display.drawTextCentered(display.width()/2, 18, _version_info); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE); } @@ -163,7 +163,7 @@ public: // display.print(filtered_name); - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.setCursor(0, 10); @@ -180,19 +180,19 @@ public: display.drawTextCentered(display.width() / 2, 54, tmp); #endif if (_task->hasConnection()) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >"); } else if (the_mesh.getBLEPin() != 0) { // BT pin - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, display.height()-8, tmp); } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); int y = 8; for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; @@ -216,7 +216,7 @@ public: display.print(tmp); } } else if (_page == HomePage::RADIO) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(1); // frequency and spreading factor display.setCursor(0, 8); @@ -238,14 +238,14 @@ public: display.drawTextRightAlign(display.width(), 26, tmp); } else if (_page == HomePage::BLUETOOTH) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); } else if (_page == HomePage::ADVERT) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32); // display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 @@ -264,9 +264,11 @@ public: #else strcpy(buf, gps_state ? "gps on" : "gps off"); #endif + display.setColor(UIColor::primary_txt); display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { // y = y + 8; + display.setColor(UIColor::warning_txt); display.drawTextLeftAlign(0, y, "Can't access GPS"); } else { if (!gps_state || !nmea->isValid()) { @@ -274,6 +276,7 @@ public: } else { sprintf(buf, "%d sat", nmea->satellitesCount()); } + display.setColor(UIColor::primary_txt); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 8; sprintf(buf, "lat %.4f", @@ -349,8 +352,10 @@ public: r.skipData(type); strcpy(name, "unk"); sprintf(buf, ""); } + display.setColor(UIColor::secondary_txt); display.setCursor(0, y); display.print(name); + display.setColor(UIColor::primary_txt); display.setCursor( display.width()-display.getTextWidth(buf)-1, y ); @@ -361,7 +366,7 @@ public: else sensors_scroll_offset = 0; #endif } else if (_page == HomePage::SHUTDOWN) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); if (_shutdown_init) { display.drawTextCentered(display.width() / 2, 20, "hibernating..."); @@ -692,9 +697,9 @@ void UITask::loop() { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; - _display->setColor(DisplayDriver::DARK); + _display->setColor(UIColor::popup_bkg); _display->fillRect(p, y, _display->width() - p*2, y); - _display->setColor(DisplayDriver::LIGHT); // draw box border + _display->setColor(UIColor::popup_txt); // draw box border _display->drawRect(p, y, _display->width() - p*2, y); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _next_refresh = _alert_expiry; // will need refresh when alert is dismissed diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 6751aad69..e7225557d 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -57,18 +57,17 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); _display->drawTextCentered(_display->width() / 2, 22, website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); _display->drawTextCentered(_display->width() / 2, 35, _version_info); @@ -77,13 +76,13 @@ void UITask::renderCurrScreen() { _display->drawTextCentered(_display->width() / 2, 48, node_type); } else if (_powering_off_at > 0) { // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); _display->drawTextCentered(_display->width()/ 2, 22, website); @@ -95,12 +94,11 @@ void UITask::renderCurrScreen() { } else { _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index fb66b8191..8e4c2fcf6 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -49,20 +49,19 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t websiteWidth = _display->getTextWidth(website); _display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->print(website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); uint16_t versionWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - versionWidth) / 2, 35); @@ -77,12 +76,11 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp index 68a80607d..75a0b4650 100644 --- a/examples/simple_sensor/UITask.cpp +++ b/examples/simple_sensor/UITask.cpp @@ -49,20 +49,19 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t websiteWidth = _display->getTextWidth(website); _display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->print(website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); uint16_t versionWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - versionWidth) / 2, 35); @@ -77,12 +76,11 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index dcc5fe031..b76a1b6ca 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -3,12 +3,20 @@ #include #include +using ColorVal = uint16_t; + +class UIColor { +public: + // color definitions (by element _type_) + static ColorVal window_bkg, title_bkg, title_txt, primary_txt, secondary_txt, warning_txt, popup_bkg, popup_txt, corp_blue; +}; + class DisplayDriver { int _w, _h; protected: DisplayDriver(int w, int h) { _w = w; _h = h; } public: - enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light + //enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light int width() const { return _w; } int height() const { return _h; } @@ -18,9 +26,9 @@ public: virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; - virtual void startFrame(Color bkg = DARK) = 0; + virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; - virtual void setColor(Color c) = 0; + virtual void setColor(ColorVal c) = 0; virtual void setCursor(int x, int y) = 0; virtual void print(const char* str) = 0; virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override diff --git a/src/helpers/ui/E213Display.cpp b/src/helpers/ui/E213Display.cpp index 814693a06..ab7429769 100644 --- a/src/helpers/ui/E213Display.cpp +++ b/src/helpers/ui/E213Display.cpp @@ -2,6 +2,17 @@ #include "../../MeshCore.h" +// Color scheme +ColorVal UIColor::window_bkg = WHITE; +ColorVal UIColor::title_bkg = BLACK; +ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::primary_txt = BLACK; +ColorVal UIColor::secondary_txt = BLACK; +ColorVal UIColor::warning_txt = BLACK; +ColorVal UIColor::popup_bkg = BLACK; +ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::corp_blue = BLACK; + BaseDisplay* E213Display::detectEInk() { // Test 1: Logic of BUSY pin @@ -108,16 +119,18 @@ void E213Display::clear() { display->clear(); } -void E213Display::startFrame(Color bkg) { +void E213Display::startFrame(ColorVal bkg) { display_crc.reset(); // Fill screen with white first to ensure clean background display->fillRect(0, 0, width(), height(), WHITE); - if (bkg == LIGHT) { + if (bkg == 0) { // Fill with black if light background requested (inverted for e-ink) display->fillRect(0, 0, width(), height(), BLACK); } + _color = UIColor::primary_txt; + display->setTextColor(_color); } void E213Display::setTextSize(int sz) { @@ -126,9 +139,10 @@ void E213Display::setTextSize(int sz) { display->setTextSize(sz); } -void E213Display::setColor(Color c) { - display_crc.update(c); - // implemented in individual display methods +void E213Display::setColor(ColorVal c) { + _color = c; + display_crc.update(c); + display->setTextColor(_color); } void E213Display::setCursor(int x, int y) { @@ -147,7 +161,7 @@ void E213Display::fillRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display->fillRect(x, y, w, h, BLACK); + display->fillRect(x, y, w, h, _color); } void E213Display::drawRect(int x, int y, int w, int h) { @@ -155,7 +169,7 @@ void E213Display::drawRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display->drawRect(x, y, w, h, BLACK); + display->drawRect(x, y, w, h, _color); } void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { @@ -179,7 +193,7 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { // If the bit is set, draw the pixel if (bitSet) { - display->drawPixel(x + bx, y + by, BLACK); + display->drawPixel(x + bx, y + by, _color); } } } diff --git a/src/helpers/ui/E213Display.h b/src/helpers/ui/E213Display.h index add8f11b3..32567a79a 100644 --- a/src/helpers/ui/E213Display.h +++ b/src/helpers/ui/E213Display.h @@ -16,6 +16,7 @@ class E213Display : public DisplayDriver { RefCountedDigitalPin* _periph_power; CRC32 display_crc; uint32_t last_display_crc_value = 0; + uint16_t _color; public: E213Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(250, 122), _periph_power(periph_power) {} @@ -30,9 +31,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/E290Display.cpp b/src/helpers/ui/E290Display.cpp index ef4df05ed..ddb449f46 100644 --- a/src/helpers/ui/E290Display.cpp +++ b/src/helpers/ui/E290Display.cpp @@ -2,6 +2,17 @@ #include "../../MeshCore.h" +// Color scheme +ColorVal UIColor::window_bkg = WHITE; +ColorVal UIColor::title_bkg = BLACK; +ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::primary_txt = BLACK; +ColorVal UIColor::secondary_txt = BLACK; +ColorVal UIColor::warning_txt = BLACK; +ColorVal UIColor::popup_bkg = BLACK; +ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::corp_blue = BLACK; + bool E290Display::begin() { if (_init) return true; @@ -62,15 +73,17 @@ void E290Display::clear() { display.clear(); } -void E290Display::startFrame(Color bkg) { +void E290Display::startFrame(ColorVal bkg) { display_crc.reset(); // Fill screen with white first to ensure clean background display.fillRect(0, 0, width(), height(), WHITE); - if (bkg == LIGHT) { + if (bkg == 0) { // Fill with black if light background requested (inverted for e-ink) display.fillRect(0, 0, width(), height(), BLACK); } + _color = UIColor::primary_txt; + display.setTextColor(_color); } void E290Display::setTextSize(int sz) { @@ -79,9 +92,10 @@ void E290Display::setTextSize(int sz) { display.setTextSize(sz); } -void E290Display::setColor(Color c) { - display_crc.update(c); - // implemented in individual display methods +void E290Display::setColor(ColorVal c) { + _color = c; + display_crc.update(c); + display.setTextColor(_color); } void E290Display::setCursor(int x, int y) { @@ -100,7 +114,7 @@ void E290Display::fillRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.fillRect(x, y, w, h, BLACK); + display.fillRect(x, y, w, h, _color); } void E290Display::drawRect(int x, int y, int w, int h) { @@ -108,7 +122,7 @@ void E290Display::drawRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.drawRect(x, y, w, h, BLACK); + display.drawRect(x, y, w, h, _color); } void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { @@ -132,7 +146,7 @@ void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { // If the bit is set, draw the pixel if (bitSet) { - display.drawPixel(x + bx, y + by, BLACK); + display.drawPixel(x + bx, y + by, _color); } } } diff --git a/src/helpers/ui/E290Display.h b/src/helpers/ui/E290Display.h index 88bf34ff1..bf6296bb7 100644 --- a/src/helpers/ui/E290Display.h +++ b/src/helpers/ui/E290Display.h @@ -16,6 +16,7 @@ class E290Display : public DisplayDriver { RefCountedDigitalPin* _periph_power; CRC32 display_crc; uint32_t last_display_crc_value = 0; + uint16_t _color; public: E290Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(296, 128), _periph_power(periph_power) {} @@ -26,9 +27,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index ad47754bf..0a5f6a558 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,18 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +// Color scheme +ColorVal UIColor::window_bkg = GxEPD_WHITE; +ColorVal UIColor::title_bkg = GxEPD_BLACK; +ColorVal UIColor::title_txt = GxEPD_WHITE; +ColorVal UIColor::primary_txt = GxEPD_BLACK; +ColorVal UIColor::secondary_txt = GxEPD_BLACK; +ColorVal UIColor::warning_txt = GxEPD_BLACK; +ColorVal UIColor::popup_bkg = GxEPD_BLACK; +ColorVal UIColor::popup_txt = GxEPD_WHITE; +ColorVal UIColor::corp_blue = GxEPD_BLACK; + + bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -61,9 +73,9 @@ void GxEPDDisplay::clear() { display_crc.reset(); } -void GxEPDDisplay::startFrame(Color bkg) { - display.fillScreen(GxEPD_WHITE); - display.setTextColor(_curr_color = GxEPD_BLACK); +void GxEPDDisplay::startFrame(ColorVal bkg) { + display.fillScreen(bkg); + display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); } @@ -85,14 +97,9 @@ void GxEPDDisplay::setTextSize(int sz) { } } -void GxEPDDisplay::setColor(Color c) { - display_crc.update (c); - // colours need to be inverted for epaper displays - if (c == DARK) { - display.setTextColor(_curr_color = GxEPD_WHITE); - } else { - display.setTextColor(_curr_color = GxEPD_BLACK); - } +void GxEPDDisplay::setColor(ColorVal c) { + display_crc.update (c); + display.setTextColor(_curr_color = c); } void GxEPDDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 219b60764..c653eac4b 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -51,9 +51,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/LGFXDisplay.cpp b/src/helpers/ui/LGFXDisplay.cpp index a53cbc620..7bdd52a25 100644 --- a/src/helpers/ui/LGFXDisplay.cpp +++ b/src/helpers/ui/LGFXDisplay.cpp @@ -1,5 +1,16 @@ #include "LGFXDisplay.h" +// Color scheme +ColorVal UIColor::window_bkg = 0xFFFF; +ColorVal UIColor::title_bkg = 0x001F; +ColorVal UIColor::title_txt = 0xFFFF; +ColorVal UIColor::primary_txt = 0x0000; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = 0xFD20; +ColorVal UIColor::popup_bkg = 0x07FF; // CYAN +ColorVal UIColor::popup_txt = 0x0000; +ColorVal UIColor::corp_blue = 0x001A; + bool LGFXDisplay::begin() { turnOn(); display->init(); @@ -35,45 +46,20 @@ void LGFXDisplay::clear() { buffer.clearDisplay(); } -void LGFXDisplay::startFrame(Color bkg) { +void LGFXDisplay::startFrame(ColorVal bkg) { // display->startWrite(); // display->getScanLine(); - buffer.clearDisplay(); - buffer.setTextColor(TFT_WHITE); + _color = bkg; + buffer.fillScreen(_color); + buffer.setTextColor(_color = UIColor::primary_txt); } void LGFXDisplay::setTextSize(int sz) { buffer.setTextSize(sz); } -void LGFXDisplay::setColor(Color c) { - // _color = (c != 0) ? ILI9342_WHITE : ILI9342_BLACK; - switch (c) { - case DARK: - _color = TFT_BLACK; - break; - case LIGHT: - _color = TFT_WHITE; - break; - case RED: - _color = TFT_RED; - break; - case GREEN: - _color = TFT_GREEN; - break; - case BLUE: - _color = TFT_BLUE; - break; - case YELLOW: - _color = TFT_YELLOW; - break; - case ORANGE: - _color = TFT_ORANGE; - break; - default: - _color = TFT_WHITE; - } - buffer.setTextColor(_color); +void LGFXDisplay::setColor(ColorVal c) { + buffer.setTextColor(_color = c); } void LGFXDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/LGFXDisplay.h b/src/helpers/ui/LGFXDisplay.h index ad7212ecf..a2d660b22 100644 --- a/src/helpers/ui/LGFXDisplay.h +++ b/src/helpers/ui/LGFXDisplay.h @@ -25,9 +25,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/NV3001BDisplay.cpp b/src/helpers/ui/NV3001BDisplay.cpp index 03825cc03..1ff7765cb 100644 --- a/src/helpers/ui/NV3001BDisplay.cpp +++ b/src/helpers/ui/NV3001BDisplay.cpp @@ -96,18 +96,16 @@ #define NV3001B_TEXT_SIZE2_SCALE_Y 3 #endif -static uint16_t mapColor(DisplayDriver::Color c) { - switch (c) { - case DisplayDriver::DARK: return 0x0000; - case DisplayDriver::LIGHT: return 0xffff; - case DisplayDriver::RED: return 0xf800; - case DisplayDriver::GREEN: return 0x07e0; - case DisplayDriver::BLUE: return 0x001f; - case DisplayDriver::YELLOW: return 0xffe0; - case DisplayDriver::ORANGE: return 0xfd20; - default: return 0xffff; - } -} +// Color scheme +ColorVal UIColor::window_bkg = 0xFFFF; +ColorVal UIColor::title_bkg = 0x001F; +ColorVal UIColor::title_txt = 0xFFFF; +ColorVal UIColor::primary_txt = 0x0000; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = 0xFD20; +ColorVal UIColor::popup_bkg = 0x07FF; // CYAN +ColorVal UIColor::popup_txt = 0x0000; +ColorVal UIColor::corp_blue = 0x001A; static int scaleX(int x) { return (int)(x * DISPLAY_SCALE_X); @@ -465,15 +463,15 @@ void NV3001BDisplay::turnOff() { void NV3001BDisplay::clear() { uint16_t saved = color; - color = 0x0000; + color = UIColor::window_bkg; fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); color = saved; } -void NV3001BDisplay::startFrame(Color bkg) { - color = mapColor(bkg); +void NV3001BDisplay::startFrame(ColorVal bkg) { + color = bkg; fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); - color = 0xffff; + color = UIColor::primary_txt; text_size = 1; cursor_x = 0; cursor_y = 0; @@ -483,8 +481,8 @@ void NV3001BDisplay::setTextSize(int sz) { text_size = sz < 1 ? 1 : sz; } -void NV3001BDisplay::setColor(Color c) { - color = mapColor(c); +void NV3001BDisplay::setColor(ColorVal c) { + color = c; } void NV3001BDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/NV3001BDisplay.h b/src/helpers/ui/NV3001BDisplay.h index 98cdaae82..76505b5d2 100644 --- a/src/helpers/ui/NV3001BDisplay.h +++ b/src/helpers/ui/NV3001BDisplay.h @@ -55,9 +55,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/NullDisplayDriver.h b/src/helpers/ui/NullDisplayDriver.h index 2a9670bd2..ff214c883 100644 --- a/src/helpers/ui/NullDisplayDriver.h +++ b/src/helpers/ui/NullDisplayDriver.h @@ -11,9 +11,9 @@ public: void turnOn() override { } void turnOff() override { } void clear() override { } - void startFrame(Color bkg = DARK) override { } + void startFrame(ColorVal bkg = UIColor::window_bkg) override { } void setTextSize(int sz) override { } - void setColor(Color c) override { } + void setColor(ColorVal c) override { } void setCursor(int x, int y) override { } void print(const char* str) override { } void fillRect(int x, int y, int w, int h) override { } diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 57ccfc2e4..aeb4f300f 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -9,6 +9,17 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) return (error == 0); } +// Color scheme +ColorVal UIColor::window_bkg = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_WHITE; +ColorVal UIColor::title_txt = SH110X_BLACK; +ColorVal UIColor::primary_txt = SH110X_WHITE; +ColorVal UIColor::secondary_txt = SH110X_WHITE; +ColorVal UIColor::warning_txt = SH110X_WHITE; +ColorVal UIColor::popup_bkg = SH110X_WHITE; +ColorVal UIColor::popup_txt = SH110X_BLACK; +ColorVal UIColor::corp_blue = SH110X_WHITE; + bool SH1106Display::begin() { // Wire must already be initialised by board.begin() before this is called. @@ -35,7 +46,7 @@ void SH1106Display::clear() display.display(); } -void SH1106Display::startFrame(Color bkg) +void SH1106Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SH110X_WHITE; @@ -49,9 +60,9 @@ void SH1106Display::setTextSize(int sz) display.setTextSize(sz); } -void SH1106Display::setColor(Color c) +void SH1106Display::setColor(ColorVal c) { - _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + _color = c; display.setTextColor(_color); } @@ -77,7 +88,7 @@ void SH1106Display::drawRect(int x, int y, int w, int h) void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { - display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); + display.drawBitmap(x, y, bits, w, h, _color); } uint16_t SH1106Display::getTextWidth(const char *str) diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index b52a6adfb..4e269d5ea 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -30,9 +30,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 464b2642a..9b8580718 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -6,6 +6,17 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { return (error == 0); } +// Color scheme +ColorVal UIColor::window_bkg = SSD1306_BLACK; +ColorVal UIColor::title_bkg = SSD1306_WHITE; +ColorVal UIColor::title_txt = SSD1306_BLACK; +ColorVal UIColor::primary_txt = SSD1306_WHITE; +ColorVal UIColor::secondary_txt = SSD1306_WHITE; +ColorVal UIColor::warning_txt = SSD1306_WHITE; +ColorVal UIColor::popup_bkg = SSD1306_WHITE; +ColorVal UIColor::popup_txt = SSD1306_BLACK; +ColorVal UIColor::corp_blue = SSD1306_WHITE; + bool SSD1306Display::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); @@ -44,7 +55,7 @@ void SSD1306Display::clear() { display.display(); } -void SSD1306Display::startFrame(Color bkg) { +void SSD1306Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SSD1306_WHITE; display.setTextColor(_color); @@ -56,8 +67,8 @@ void SSD1306Display::setTextSize(int sz) { display.setTextSize(sz); } -void SSD1306Display::setColor(Color c) { - _color = (c != 0) ? SSD1306_WHITE : SSD1306_BLACK; +void SSD1306Display::setColor(ColorVal c) { + _color = c; display.setTextColor(_color); } @@ -78,7 +89,7 @@ void SSD1306Display::drawRect(int x, int y, int w, int h) { } void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { - display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE); + display.drawBitmap(x, y, bits, w, h, _color); } uint16_t SSD1306Display::getTextWidth(const char* str) { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index d843da85b..5dabbc58c 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -35,9 +35,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 905ff5037..344f5b1d9 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -417,6 +417,17 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #define PIN_TFT_LEDA_CTL_ACTIVE HIGH #endif +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7735Display::begin() { if (!sprite) { // alloc offscreen canvas @@ -527,9 +538,9 @@ void ST7735Display::clear() { sprite->fillScreen(ST77XX_BLACK); } -void ST7735Display::startFrame(Color bkg) { - sprite->fillScreen(ST77XX_BLACK); - sprite->setTextColor(curr_color = ST77XX_WHITE); +void ST7735Display::startFrame(ColorVal bkg) { + sprite->fillScreen(bkg); + sprite->setTextColor(curr_color = UIColor::primary_txt); sprite->setFreeFont(); sprite->setTextSize(1); // This one affects size of Please wait... message //sprite->cp437(true); // Use full 256 char 'Code Page 437' font @@ -539,33 +550,8 @@ void ST7735Display::setTextSize(int sz) { sprite->setTextSize(sz); } -void ST7735Display::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - curr_color = ST77XX_BLACK; - break; - case DisplayDriver::LIGHT : - curr_color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - curr_color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - curr_color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - curr_color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - curr_color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - curr_color = ST77XX_ORANGE; - break; - default: - curr_color = ST77XX_WHITE; - break; - } +void ST7735Display::setColor(ColorVal c) { + curr_color = c; sprite->setTextColor(curr_color); } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 68c83db25..6e289b219 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -33,9 +33,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 98d693958..fad751942 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -26,6 +26,17 @@ #define SCALE_Y DISPLAY_SCALE_Y #endif +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7789Display::begin() { if(!_isOn) { pinMode(PIN_TFT_VDD_CTL, OUTPUT); @@ -90,9 +101,9 @@ void ST7789Display::clear() { display.clear(); } -void ST7789Display::startFrame(Color bkg) { - display.clear(); - _color = ST77XX_WHITE; +void ST7789Display::startFrame(ColorVal bkg) { + display.fillRect(0, 0, display.width(), display.height()); + _color = UIColor::primary_txt; display.setRGB(_color); display.setFont(ArialMT_Plain_16); } @@ -110,38 +121,8 @@ void ST7789Display::setTextSize(int sz) { } } -void ST7789Display::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - _color = ST77XX_BLACK; - display.setColor(OLEDDISPLAY_COLOR::BLACK); - break; -#if 0 - case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - _color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - _color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - _color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; - break; -#endif - default: - _color = ST77XX_WHITE; - display.setColor(OLEDDISPLAY_COLOR::WHITE); - break; - } - display.setRGB(_color); +void ST7789Display::setColor(ColorVal c) { + display.setRGB(_color = c); } void ST7789Display::setCursor(int x, int y) { diff --git a/src/helpers/ui/ST7789Display.h b/src/helpers/ui/ST7789Display.h index 9822a67df..580ad463f 100644 --- a/src/helpers/ui/ST7789Display.h +++ b/src/helpers/ui/ST7789Display.h @@ -27,9 +27,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void printWordWrap(const char* str, int max_width) override; diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index 7a02668bc..a9f30dd56 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -23,6 +23,17 @@ bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) { return true; } +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7789LCDDisplay::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); @@ -78,9 +89,9 @@ void ST7789LCDDisplay::clear() { display.fillScreen(ST77XX_BLACK); } -void ST7789LCDDisplay::startFrame(Color bkg) { - display.fillScreen(ST77XX_BLACK); - display.setTextColor(ST77XX_WHITE); +void ST7789LCDDisplay::startFrame(ColorVal bkg) { + display.fillScreen(bkg); + display.setTextColor(_color = UIColor::primary_txt); display.setTextSize(1 * DISPLAY_SCALE_X); // This one affects size of Please wait... message display.cp437(true); // Use full 256 char 'Code Page 437' font } @@ -89,34 +100,8 @@ void ST7789LCDDisplay::setTextSize(int sz) { display.setTextSize(sz * DISPLAY_SCALE_X); } -void ST7789LCDDisplay::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - _color = ST77XX_BLACK; - break; - case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - _color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - _color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - _color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; - break; - default: - _color = ST77XX_WHITE; - break; - } - display.setTextColor(_color); +void ST7789LCDDisplay::setColor(ColorVal c) { + display.setTextColor(_color = c); } void ST7789LCDDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index 03a6d3f12..b5127d350 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -47,9 +47,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/U8g2Display.cpp b/src/helpers/ui/U8g2Display.cpp new file mode 100644 index 000000000..72118a0e6 --- /dev/null +++ b/src/helpers/ui/U8g2Display.cpp @@ -0,0 +1,12 @@ +#include "U8g2Display.h" + +// Color scheme +ColorVal UIColor::window_bkg = 0; +ColorVal UIColor::title_bkg = 1; +ColorVal UIColor::title_txt = 0; +ColorVal UIColor::primary_txt = 1; +ColorVal UIColor::secondary_txt = 1; +ColorVal UIColor::warning_txt = 1; +ColorVal UIColor::popup_bkg = 1; +ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::corp_blue = 1; diff --git a/src/helpers/ui/U8g2Display.h b/src/helpers/ui/U8g2Display.h index 73c589363..a87ee19b3 100644 --- a/src/helpers/ui/U8g2Display.h +++ b/src/helpers/ui/U8g2Display.h @@ -72,10 +72,10 @@ public: _u8g2.sendBuffer(); } - void startFrame(Color bkg = DARK) override { - _u8g2.clearBuffer(); - _drawColor = 1; - _u8g2.setDrawColor(1); + void startFrame(ColorVal bkg = UIColor::window_bkg) override { + _u8g2.clearBuffer(); // TODO: apply 'bkg' color + _drawColor = UIColor::primary_txt; + _u8g2.setDrawColor(_drawColor); applyFont(1); } @@ -83,8 +83,8 @@ public: applyFont(sz); } - void setColor(Color c) override { - _drawColor = (c != DARK) ? 1 : 0; + void setColor(ColorVal c) override { + _drawColor = c; _u8g2.setDrawColor(_drawColor); } @@ -94,22 +94,18 @@ public: } void print(const char* str) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawStr(_cursorX, _cursorY, str); } void fillRect(int x, int y, int w, int h) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawBox(x, y, w, h); } void drawRect(int x, int y, int w, int h) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawFrame(x, y, w, h); } void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { - _u8g2.setDrawColor(1); _u8g2.drawXBM(x, y, w, h, bits); } diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 07bcebfe4..c7bb73785 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter} + + + - + + + + +<../variants/lilygo_techo_card> lib_deps = From a96a66aa999df0c9a2ded5d99c156319360bb199 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 18:55:59 +1000 Subject: [PATCH 146/191] * build fixes for NullDisplayDriver --- src/helpers/ui/NullDisplayDriver.cpp | 11 +++++++++ variants/heltec_rc32/platformio.ini | 3 +++ variants/heltec_t114/platformio.ini | 2 ++ variants/heltec_tower_v2/platformio.ini | 2 ++ variants/ikoka_nano_nrf/platformio.ini | 5 ++-- .../minewsemi_me25ls01/NullDisplayDriver.h | 24 ------------------- variants/minewsemi_me25ls01/platformio.ini | 5 ++++ variants/minewsemi_me25ls01/target.h | 2 +- variants/t1000-e/platformio.ini | 2 ++ variants/thinknode_m3/platformio.ini | 2 ++ variants/thinknode_m7/platformio.ini | 2 ++ variants/wio-e5-mini/NullDisplayDriver.h | 24 ------------------- variants/wio-e5-mini/platformio.ini | 1 + variants/wio-e5-mini/target.h | 2 +- 14 files changed, 34 insertions(+), 53 deletions(-) create mode 100644 src/helpers/ui/NullDisplayDriver.cpp delete mode 100644 variants/minewsemi_me25ls01/NullDisplayDriver.h delete mode 100644 variants/wio-e5-mini/NullDisplayDriver.h diff --git a/src/helpers/ui/NullDisplayDriver.cpp b/src/helpers/ui/NullDisplayDriver.cpp new file mode 100644 index 000000000..1500f5c1b --- /dev/null +++ b/src/helpers/ui/NullDisplayDriver.cpp @@ -0,0 +1,11 @@ +#include "NullDisplayDriver.h" + +ColorVal UIColor::window_bkg = 0; +ColorVal UIColor::title_bkg = 0; +ColorVal UIColor::title_txt = 0; +ColorVal UIColor::primary_txt = 0; +ColorVal UIColor::secondary_txt = 0; +ColorVal UIColor::warning_txt = 0; +ColorVal UIColor::popup_bkg = 0; +ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::corp_blue = 0; diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index d535fb77c..6361f61de 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -134,6 +134,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -157,6 +158,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -180,6 +182,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 135babb1a..48dfe0781 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -109,6 +109,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t114.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -128,6 +129,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 build_src_filter = ${Heltec_t114.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index f029b7d41..9c703ae1c 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -69,6 +69,7 @@ build_flags = ; -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> @@ -91,6 +92,7 @@ build_flags = ; -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> diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index e72f83ce0..7880ea4a3 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -31,6 +31,8 @@ debug_tool = jlink upload_protocol = nrfutil lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} +build_src_filter = ${nrf52_base.build_src_filter} + + [ikoka_nano_nrf_e22_22dbm] extends = ikoka_nano_nrf @@ -42,7 +44,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_e22_30dbm] @@ -56,7 +57,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_e22_33dbm] @@ -70,7 +70,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_companion_radio_ble] diff --git a/variants/minewsemi_me25ls01/NullDisplayDriver.h b/variants/minewsemi_me25ls01/NullDisplayDriver.h deleted file mode 100644 index 38bf93f15..000000000 --- a/variants/minewsemi_me25ls01/NullDisplayDriver.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -class NullDisplayDriver : public DisplayDriver { -public: - NullDisplayDriver() : DisplayDriver(128, 64) { } - bool begin() { return false; } // not present - - bool isOn() override { return false; } - void turnOn() override { } - void turnOff() override { } - void clear() override { } - void startFrame(Color bkg = DARK) override { } - void setTextSize(int sz) override { } - void setColor(Color c) override { } - void setCursor(int x, int y) override { } - void print(const char* str) override { } - void fillRect(int x, int y, int w, int h) override { } - void drawRect(int x, int y, int w, int h) override { } - void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } - uint16_t getTextWidth(const char* str) override { return 0; } - void endFrame() { } -}; diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index 39d4252de..ac81fa16b 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -55,6 +55,7 @@ build_flags = ${me25ls01.build_flags} ;-D PIN_BUZZER=25 ;-D PIN_BUZZER_EN=37 build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> @@ -79,6 +80,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_repeater> + + [env:Minewsemi_me25ls01_room_server] extends = me25ls01 @@ -101,6 +103,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_room_server> + + [env:Minewsemi_me25ls01_terminal_chat] extends = me25ls01 @@ -123,6 +126,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_secure_chat/main.cpp> + + [env:Minewsemi_me25ls01_companion_radio_usb] extends = me25ls01 @@ -142,6 +146,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index f8d42863b..978e616b9 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -10,7 +10,7 @@ #include #include #ifdef DISPLAY_CLASS - #include "NullDisplayDriver.h" + #include #endif #ifdef DISPLAY_CLASS diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 43a3d93f6..6f32e54d0 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -84,6 +84,7 @@ build_flags = ${t1000-e.build_flags} -D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E build_src_filter = ${t1000-e.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${t1000-e.lib_deps} @@ -112,6 +113,7 @@ build_flags = ${t1000-e.build_flags} build_src_filter = ${t1000-e.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${t1000-e.lib_deps} diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0004f4c73..f80a61618 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -85,6 +85,7 @@ build_flags = ${ThinkNode_M3.build_flags} -D PIN_BUZZER_EN=36 build_src_filter = ${ThinkNode_M3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M3.lib_deps} @@ -111,6 +112,7 @@ build_flags = ${ThinkNode_M3.build_flags} build_src_filter = ${ThinkNode_M3.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M3.lib_deps} diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index af5d8ea01..acd98c8c7 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -83,6 +83,7 @@ build_flags = build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = @@ -122,6 +123,7 @@ build_flags = build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/wio-e5-mini/NullDisplayDriver.h b/variants/wio-e5-mini/NullDisplayDriver.h deleted file mode 100644 index 2a9670bd2..000000000 --- a/variants/wio-e5-mini/NullDisplayDriver.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -class NullDisplayDriver : public DisplayDriver { -public: - NullDisplayDriver() : DisplayDriver(128, 64) { } - bool begin() { return false; } // not present - - bool isOn() override { return false; } - void turnOn() override { } - void turnOff() override { } - void clear() override { } - void startFrame(Color bkg = DARK) override { } - void setTextSize(int sz) override { } - void setColor(Color c) override { } - void setCursor(int x, int y) override { } - void print(const char* str) override { } - void fillRect(int x, int y, int w, int h) override { } - void drawRect(int x, int y, int w, int h) override { } - void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } - uint16_t getTextWidth(const char* str) override { return 0; } - void endFrame() { } -}; diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 82f01331b..7cf261987 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -45,6 +45,7 @@ build_flags = ${lora_e5_mini.build_flags} -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${lora_e5_mini.build_src_filter} + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${lora_e5_mini.lib_deps} diff --git a/variants/wio-e5-mini/target.h b/variants/wio-e5-mini/target.h index 4807e0f79..f4d6a0884 100644 --- a/variants/wio-e5-mini/target.h +++ b/variants/wio-e5-mini/target.h @@ -8,7 +8,7 @@ #include #include #ifdef DISPLAY_CLASS - #include "NullDisplayDriver.h" + #include #endif #include From 56274db4ad7ccff236b7cb908927fe6161df2e9e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 19:52:23 +1000 Subject: [PATCH 147/191] * build fixes for old ESP32 boards --- examples/simple_repeater/MyMesh.h | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0b2e7491b..aa7d30b06 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -11,6 +11,7 @@ #include #elif defined(ESP32) #include + using File = fs::File; #endif #ifdef WITH_RS232_BRIDGE From 783b21bb9ff3770851a634e5c18eb77384ba7e0b Mon Sep 17 00:00:00 2001 From: Nick Dunklee Date: Thu, 23 Jul 2026 14:02:34 -0600 Subject: [PATCH 148/191] Global nrf52 hardware crypto, removed from individual configs Select nodes had this flag enabled, testing by the community and hardware specs indicate this can be enabled global for all node types using this chipset. Any nodes down the line that may be quirky can be individually disabled with `-U USE_CC310_HW_CRYPTO`. --- platformio.ini | 1 + variants/heltec_t096/platformio.ini | 1 - variants/rak3401/platformio.ini | 1 - variants/rak4631/platformio.ini | 1 - variants/t1000-e/platformio.ini | 1 - 5 files changed, 1 insertion(+), 4 deletions(-) diff --git a/platformio.ini b/platformio.ini index f3ada1338..c32549405 100644 --- a/platformio.ini +++ b/platformio.ini @@ -91,6 +91,7 @@ build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM -D LFS_NO_ASSERT=1 -D EXTRAFS=1 + -D USE_CC310_HW_CRYPTO=1 lib_deps = ${arduino_base.lib_deps} https://github.com/oltaco/CustomLFS#0.2.2 diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 2f440e837..0a062f00b 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -29,7 +29,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D USE_CC310_HW_CRYPTO=1 -D PIN_VEXT_EN=26 ; Vext is connected to VDD which is also connected to TFT & GPS -D PIN_VEXT_EN_ACTIVE=HIGH -D PIN_GPS_RX=25 diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index ea53b2dc5..20a8a548b 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -13,7 +13,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM - -D USE_CC310_HW_CRYPTO=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 58055ab45..31b507b4c 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -22,7 +22,6 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D USE_CC310_HW_CRYPTO=1 -D ENV_INCLUDE_RAK12035=1 -UENV_INCLUDE_BME680 -D ENV_INCLUDE_BME680_BSEC=1 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index d5e3bb535..43a3d93f6 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -18,7 +18,6 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D RF_SWITCH_TABLE -D RX_BOOSTED_GAIN=true - -D USE_CC310_HW_CRYPTO=1 -D P_LORA_BUSY=7 ; P0.7 -D P_LORA_SCLK=11 ; P0.11 -D P_LORA_NSS=12 ; P0.12 From 520fa77f4fedfe0d39eb0c472f7c7346f470df82 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 24 Jul 2026 15:31:01 +1200 Subject: [PATCH 149/191] increase tbeam sx1276 max group channels from 8 to 40 --- variants/lilygo_tbeam_SX1276/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index cb25903ce..f5f490479 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -45,7 +45,7 @@ build_flags = ${LilyGo_TBeam_SX1276.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=160 - -D MAX_GROUP_CHANNELS=8 + -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 From 3857f4582643ab99e4210a60f05b5963a728cd0a Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 15:14:56 +1000 Subject: [PATCH 150/191] * monochrome displays now do _not_ invert titlebar --- src/helpers/ui/E213Display.cpp | 8 ++++---- src/helpers/ui/E290Display.cpp | 8 ++++---- src/helpers/ui/GxEPDDisplay.cpp | 8 ++++---- src/helpers/ui/SH1106Display.cpp | 8 ++++---- src/helpers/ui/SSD1306Display.cpp | 8 ++++---- src/helpers/ui/U8g2Display.cpp | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/helpers/ui/E213Display.cpp b/src/helpers/ui/E213Display.cpp index ab7429769..daf26989b 100644 --- a/src/helpers/ui/E213Display.cpp +++ b/src/helpers/ui/E213Display.cpp @@ -4,13 +4,13 @@ // Color scheme ColorVal UIColor::window_bkg = WHITE; -ColorVal UIColor::title_bkg = BLACK; -ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::title_bkg = WHITE; +ColorVal UIColor::title_txt = BLACK; ColorVal UIColor::primary_txt = BLACK; ColorVal UIColor::secondary_txt = BLACK; ColorVal UIColor::warning_txt = BLACK; -ColorVal UIColor::popup_bkg = BLACK; -ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::popup_bkg = WHITE; +ColorVal UIColor::popup_txt = BLACK; ColorVal UIColor::corp_blue = BLACK; BaseDisplay* E213Display::detectEInk() diff --git a/src/helpers/ui/E290Display.cpp b/src/helpers/ui/E290Display.cpp index ddb449f46..34bbcdbc5 100644 --- a/src/helpers/ui/E290Display.cpp +++ b/src/helpers/ui/E290Display.cpp @@ -4,13 +4,13 @@ // Color scheme ColorVal UIColor::window_bkg = WHITE; -ColorVal UIColor::title_bkg = BLACK; -ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::title_bkg = WHITE; +ColorVal UIColor::title_txt = BLACK; ColorVal UIColor::primary_txt = BLACK; ColorVal UIColor::secondary_txt = BLACK; ColorVal UIColor::warning_txt = BLACK; -ColorVal UIColor::popup_bkg = BLACK; -ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::popup_bkg = WHITE; +ColorVal UIColor::popup_txt = BLACK; ColorVal UIColor::corp_blue = BLACK; bool E290Display::begin() { diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 0a5f6a558..13dafa345 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -16,13 +16,13 @@ // Color scheme ColorVal UIColor::window_bkg = GxEPD_WHITE; -ColorVal UIColor::title_bkg = GxEPD_BLACK; -ColorVal UIColor::title_txt = GxEPD_WHITE; +ColorVal UIColor::title_bkg = GxEPD_WHITE; +ColorVal UIColor::title_txt = GxEPD_BLACK; ColorVal UIColor::primary_txt = GxEPD_BLACK; ColorVal UIColor::secondary_txt = GxEPD_BLACK; ColorVal UIColor::warning_txt = GxEPD_BLACK; -ColorVal UIColor::popup_bkg = GxEPD_BLACK; -ColorVal UIColor::popup_txt = GxEPD_WHITE; +ColorVal UIColor::popup_bkg = GxEPD_WHITE; +ColorVal UIColor::popup_txt = GxEPD_BLACK; ColorVal UIColor::corp_blue = GxEPD_BLACK; diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index aeb4f300f..c3840c02a 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,13 +11,13 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) // Color scheme ColorVal UIColor::window_bkg = SH110X_BLACK; -ColorVal UIColor::title_bkg = SH110X_WHITE; -ColorVal UIColor::title_txt = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_BLACK; +ColorVal UIColor::title_txt = SH110X_WHITE; ColorVal UIColor::primary_txt = SH110X_WHITE; ColorVal UIColor::secondary_txt = SH110X_WHITE; ColorVal UIColor::warning_txt = SH110X_WHITE; -ColorVal UIColor::popup_bkg = SH110X_WHITE; -ColorVal UIColor::popup_txt = SH110X_BLACK; +ColorVal UIColor::popup_bkg = SH110X_BLACK; +ColorVal UIColor::popup_txt = SH110X_WHITE; ColorVal UIColor::corp_blue = SH110X_WHITE; bool SH1106Display::begin() diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 9b8580718..ab2d77fbc 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -8,13 +8,13 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { // Color scheme ColorVal UIColor::window_bkg = SSD1306_BLACK; -ColorVal UIColor::title_bkg = SSD1306_WHITE; -ColorVal UIColor::title_txt = SSD1306_BLACK; +ColorVal UIColor::title_bkg = SSD1306_BLACK; +ColorVal UIColor::title_txt = SSD1306_WHITE; ColorVal UIColor::primary_txt = SSD1306_WHITE; ColorVal UIColor::secondary_txt = SSD1306_WHITE; ColorVal UIColor::warning_txt = SSD1306_WHITE; -ColorVal UIColor::popup_bkg = SSD1306_WHITE; -ColorVal UIColor::popup_txt = SSD1306_BLACK; +ColorVal UIColor::popup_bkg = SSD1306_BLACK; +ColorVal UIColor::popup_txt = SSD1306_WHITE; ColorVal UIColor::corp_blue = SSD1306_WHITE; bool SSD1306Display::begin() { diff --git a/src/helpers/ui/U8g2Display.cpp b/src/helpers/ui/U8g2Display.cpp index 72118a0e6..e8d7fe2f1 100644 --- a/src/helpers/ui/U8g2Display.cpp +++ b/src/helpers/ui/U8g2Display.cpp @@ -2,11 +2,11 @@ // Color scheme ColorVal UIColor::window_bkg = 0; -ColorVal UIColor::title_bkg = 1; -ColorVal UIColor::title_txt = 0; +ColorVal UIColor::title_bkg = 0; +ColorVal UIColor::title_txt = 1; ColorVal UIColor::primary_txt = 1; ColorVal UIColor::secondary_txt = 1; ColorVal UIColor::warning_txt = 1; -ColorVal UIColor::popup_bkg = 1; -ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::popup_bkg = 0; +ColorVal UIColor::popup_txt = 1; ColorVal UIColor::corp_blue = 1; From 0c04a473559a3ffa0c8bd7c881ac3820da28fef7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 15:31:22 +1000 Subject: [PATCH 151/191] * fixed for mono ST7789 display driver --- src/helpers/ui/ST7789Display.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index fad751942..7aff92a13 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -27,15 +27,15 @@ #endif // Color scheme -ColorVal UIColor::window_bkg = ST77XX_WHITE; -ColorVal UIColor::title_bkg = ST77XX_BLUE; -ColorVal UIColor::title_txt = ST77XX_WHITE; -ColorVal UIColor::primary_txt = ST77XX_BLACK; -ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray -ColorVal UIColor::warning_txt = ST77XX_ORANGE; -ColorVal UIColor::popup_bkg = ST77XX_CYAN; -ColorVal UIColor::popup_txt = ST77XX_BLACK; -ColorVal UIColor::corp_blue = 0x001A; +ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::BLACK; bool ST7789Display::begin() { if(!_isOn) { @@ -102,9 +102,8 @@ void ST7789Display::clear() { } void ST7789Display::startFrame(ColorVal bkg) { - display.fillRect(0, 0, display.width(), display.height()); - _color = UIColor::primary_txt; - display.setRGB(_color); + display.clear(); // TODO: use bkg + setColor(UIColor::primary_txt); display.setFont(ArialMT_Plain_16); } @@ -122,7 +121,9 @@ void ST7789Display::setTextSize(int sz) { } void ST7789Display::setColor(ColorVal c) { - display.setRGB(_color = c); + _color = c; + display.setColor((OLEDDISPLAY_COLOR)_color); + display.setRGB(_color == OLEDDISPLAY_COLOR::WHITE ? ST77XX_WHITE : ST77XX_BLACK); } void ST7789Display::setCursor(int x, int y) { From f83ad03e5e79fcb54eb5b91e3675df748c3434fa Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 18:35:56 +1000 Subject: [PATCH 152/191] * T114 display fix --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- src/helpers/ui/ST7789Display.cpp | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 09903db0b..50dc91d2d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -204,7 +204,11 @@ public: renderBatteryIndicator(display, _task->getBattMilliVolts()); // curr page indicator - display.setColor(UIColor::title_bkg); + if (UIColor::title_bkg == UIColor::window_bkg) { + display.setColor(UIColor::title_txt); + } else { + display.setColor(UIColor::title_bkg); + } int y = 14; int x = display.width() / 2 - 5 * (HomePage::Count-1); for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 7aff92a13..7d039a131 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -27,15 +27,15 @@ #endif // Color scheme -ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::WHITE; bool ST7789Display::begin() { if(!_isOn) { From fae480315829ded473ffefc8e7d8988f8830b7d8 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 20:50:38 +1000 Subject: [PATCH 153/191] SX1262: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSX1262.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 15efed5ca..6a7c21440 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -100,6 +100,11 @@ class CustomSX1262 : public SX1262 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return SX1262::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint32_t irq = getIrqFlags(); bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 From ea5d7c8bf5b93b25ecb718b3654a3736a483308b Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:46:36 +1000 Subject: [PATCH 154/191] LR1110: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomLR1110.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 0470e48f2..20aea9ff3 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -35,6 +35,11 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return LR1110::startReceive(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint32_t irq = getIrqStatus(); bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 From c56928347f980ac8aa654ba0c34eed030c990b93 Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:18:59 +1000 Subject: [PATCH 155/191] LLCC68: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomLLCC68.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index 686b09ecd..bde44b6ca 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -2,9 +2,6 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomLLCC68 : public LLCC68 { public: CustomLLCC68(Module *mod) : LLCC68(mod) { } @@ -78,9 +75,14 @@ class CustomLLCC68 : public LLCC68 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return LLCC68::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } From 0d2b988f24470ce2f0c6ea3343fc4d2144364627 Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:20:02 +1000 Subject: [PATCH 156/191] STM32WLx: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSTM32WLx.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomSTM32WLx.h b/src/helpers/radiolib/CustomSTM32WLx.h index cbdd5c8c4..01190d065 100644 --- a/src/helpers/radiolib/CustomSTM32WLx.h +++ b/src/helpers/radiolib/CustomSTM32WLx.h @@ -2,16 +2,18 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSTM32WLx : public STM32WLx { public: CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return STM32WLx::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } }; \ No newline at end of file From 6b83de507d946c01322191233dbabb9b77d7712c Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:20:57 +1000 Subject: [PATCH 157/191] SX1268: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSX1268.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index 0c6f828b0..1e71a3fa6 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -2,9 +2,6 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSX1268 : public SX1268 { public: CustomSX1268(Module *mod) : SX1268(mod) { } @@ -78,9 +75,14 @@ class CustomSX1268 : public SX1268 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return SX1268::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } From b62b421456fdbd7c86d6eeadbabf8db48bd054cc Mon Sep 17 00:00:00 2001 From: Talia Date: Sat, 25 Jul 2026 20:55:58 -0700 Subject: [PATCH 158/191] docs: increase header levels on payloads page to fix table of contents mkdocs will only consider the first H1 (if any) and subheaders under it for the table of contents this increases the header levels of everything below "important concepts" by 1 so that the table of contents correctly resolves them --- docs/payloads.md | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/payloads.md b/docs/payloads.md index 21cb94696..b4a98b385 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -23,7 +23,7 @@ NOTE: all 16 and 32-bit integer fields are Little Endian. * Node hash: the first byte of the node's public key -# Node advertisement +## Node advertisement This kind of payload notifies receivers that a node exists, and gives information about the node | Field | Size (bytes) | Description | @@ -57,7 +57,7 @@ Appdata Flags | `0x40` | has feature 2 | Reserved for future use. | | `0x80` | has name | appdata contains a node name | -# Acknowledgement +## Acknowledgement An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra. @@ -66,7 +66,7 @@ An acknowledgement that a message was received. Note that for returned path mess | checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey | -# Returned path, request, response, and plain text message +## Returned path, request, response, and plain text message Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation. @@ -77,7 +77,7 @@ Returned path, request, response, and plain text messages are all formatted in t | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see subsections below for details | -## Returned path +### Returned path Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message. @@ -88,7 +88,7 @@ Returned path messages provide a description of the route a packet took from the | extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md) | | extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document | -## Request +### Request | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| @@ -102,7 +102,7 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | `0x01` | get stats | get stats of repeater or room server | | `0x02` | keepalive | keep-alive request used for maintained connections | -### Get stats +#### Get stats Gets information about the node, possibly including the following: @@ -125,32 +125,32 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -### Get telemetry data +#### Get telemetry data Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. -### Get Telemetry +#### Get Telemetry Not defined in `BaseChatMesh`. -### Get Min/Max/Ave (Sensor nodes) +#### Get Min/Max/Ave (Sensor nodes) Not defined in `BaseChatMesh`. -### Get Access List +#### Get Access List Not defined in `BaseChatMesh`. -### Get Neighbors +#### Get Neighbors Not defined in `BaseChatMesh`. -### Get Owner Info +#### Get Owner Info Not defined in `BaseChatMesh`. -## Response +### Response | Field | Size (bytes) | Description | |---------|-----------------|-----------------------------------| @@ -158,7 +158,7 @@ Not defined in `BaseChatMesh`. Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above. -## Plain text message +### Plain text message | Field | Size (bytes) | Description | |--------------------|-----------------|-----------------------------------------------------------------------------------| @@ -174,7 +174,7 @@ txt_type | `0x01` | CLI command | the command text of the message | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | -# Anonymous request +## Anonymous request | Field | Size (bytes) | Description | |------------------|-----------------|-------------------------------------------| @@ -183,7 +183,7 @@ txt_type | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see below for details | -## Room server login +### Room server login | Field | Size (bytes) | Description | |----------------|-----------------|-------------------------------------------------------------------------------| @@ -191,14 +191,14 @@ txt_type | sync timestamp | 4 | sender's "sync messages SINCE x" timestamp | | password | rest of message | password for room | -## Repeater/Sensor login +### Repeater/Sensor login | Field | Size (bytes) | Description | |----------------|-----------------|-------------------------------------------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | | password | rest of message | password for repeater/sensor | -## Repeater - Regions request +### Repeater - Regions request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -207,7 +207,7 @@ txt_type | reply path len | 1 | path len for reply | | reply path | (variable) | reply path | -## Repeater - Owner info request +### Repeater - Owner info request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -216,7 +216,7 @@ txt_type | reply path len | 1 | path len for reply | | reply path | (variable) | reply path | -## Repeater - Clock and status request +### Repeater - Clock and status request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -226,7 +226,7 @@ txt_type | reply path | (variable) | reply path | -# Group text message +## Group text message | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -236,7 +236,7 @@ txt_type The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `: ` (eg., `user123: I'm on my way`). -# Group datagram +## Group datagram | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -253,14 +253,14 @@ The data contained in the ciphertext uses the format below: | data | rest of payload | (depends on data type) | -# Control data +## Control data | Field | Size (bytes) | Description | |--------------|-----------------|--------------------------------------------| | flags | 1 | upper 4 bits is sub_type | | data | rest of payload | typically unencrypted data | -## DISCOVER_REQ (sub_type) +### DISCOVER_REQ (sub_type) | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -269,7 +269,7 @@ The data contained in the ciphertext uses the format below: | tag | 4 | randomly generate by sender | | since | 4 | (optional) epoch timestamp (0 by default) | -## DISCOVER_RESP (sub_type) +### DISCOVER_RESP (sub_type) | Field | Size (bytes) | Description | |--------------|-----------------|--------------------------------------------| @@ -279,6 +279,6 @@ The data contained in the ciphertext uses the format below: | pubkey | 8 or 32 | node's ID (or prefix) | -# Custom packet +## Custom packet Custom packets have no defined format. From 5de857f8f830f27abb360cdb5814ae3be07e8b5d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 17 Jul 2026 23:59:27 +1200 Subject: [PATCH 159/191] initial ch390 ethernet support for thinknode m7 companion --- examples/companion_radio/main.cpp | 5 + src/helpers/ethernet/Ethernet.h | 9 + .../ethernet/ch390/CH390EthernetInterface.cpp | 207 ++++++++++++++++++ .../ethernet/ch390/CH390EthernetInterface.h | 80 +++++++ variants/thinknode_m7/platformio.ini | 36 +++ 5 files changed, 337 insertions(+) create mode 100644 src/helpers/ethernet/Ethernet.h create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.cpp create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d39aeef95..2fc00dbd2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -48,6 +48,9 @@ static uint32_t _atoi(const char* sp) { #include ArduinoSerialInterface serial_interface; HardwareSerial companion_serial(1); + #elif defined(ETHERNET_ENABLED) + #include + ETHERNET_CLASS serial_interface; #else #include ArduinoSerialInterface serial_interface; @@ -240,6 +243,8 @@ void setup() { companion_serial.setPins(SERIAL_RX, SERIAL_TX); companion_serial.begin(115200); serial_interface.begin(companion_serial); +#elif defined(ETHERNET_ENABLED) + serial_interface.begin(); #else serial_interface.begin(Serial); #endif diff --git a/src/helpers/ethernet/Ethernet.h b/src/helpers/ethernet/Ethernet.h new file mode 100644 index 000000000..4d889df45 --- /dev/null +++ b/src/helpers/ethernet/Ethernet.h @@ -0,0 +1,9 @@ +#pragma once + +#if defined(ETHERNET_ENABLED) + #if defined(ETHERNET_USE_CH390) + #include "helpers/ethernet/ch390/CH390EthernetInterface.h" + #else + #error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!" + #endif +#endif diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp new file mode 100644 index 000000000..f7ee55030 --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp @@ -0,0 +1,207 @@ +#include "CH390EthernetInterface.h" + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool CH390EthernetInterface::begin() { + ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + + // Init CH390 + ch390_config_t config = CH390_DEFAULT_CONFIG(); + config.spi_miso_gpio = ETH_MISO_PIN; + config.spi_mosi_gpio = ETH_MOSI_PIN; + config.spi_sck_gpio = ETH_SCLK_PIN; + config.spi_cs_gpio = ETH_CS_PIN; + config.int_gpio = ETH_INT_PIN; + if (!CH390.begin(config)) { + ETHERNET_DEBUG_PRINTLN("Failed to initialize CH390 hardware."); + return false; + } + + // Setup Static IP if build flags are present + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gw(ETHERNET_STATIC_GATEWAY); + IPAddress sn(ETHERNET_STATIC_SUBNET); + CH390.config(ip, gw, sn); + #endif + + // Start Server + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + + return true; +} + +void CH390EthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); +} + +void CH390EthernetInterface::disable() { + _isEnabled = false; +} + +size_t CH390EthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool CH390EthernetInterface::isWriteBusy() const { + return false; +} + +size_t CH390EthernetInterface::checkRecvFrame(uint8_t dest[]) { + if (server.hasClient()) { + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN( + "New client accepted %u.%u.%u.%u:%u", + new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); + + deviceConnected = false; + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETHERNET_DEBUG_PRINTLN("Switched to new client"); + } + } + + if (client.connected()) { + if (!deviceConnected) { + ETHERNET_DEBUG_PRINTLN( + "Got connection %u.%u.%u.%u:%u", + client.remoteIP()[0], + client.remoteIP()[1], + client.remoteIP()[2], + client.remoteIP()[3], + client.remotePort()); + deviceConnected = true; + } + } else { + if (deviceConnected) { + deviceConnected = false; + ETHERNET_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + client.write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (client.available()) { + int c = client.read(); + if (c < 0) break; + +#if ETHERNET_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} + +bool CH390EthernetInterface::isConnected() const { + return deviceConnected; +} + +void CH390EthernetInterface::loop() { + +} diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.h b/src/helpers/ethernet/ch390/CH390EthernetInterface.h new file mode 100644 index 000000000..d638319bc --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.h @@ -0,0 +1,80 @@ +#pragma once + +#include "../../BaseSerialInterface.h" +#include +#include +#include +#include +#include + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class CH390EthernetInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + WiFiServer server; + WiFiClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + CH390EthernetInterface() : server(ETHERNET_TCP_PORT) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + void loop(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} +#endif diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index acd98c8c7..43ff61c3a 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -34,6 +34,22 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 +[ThinkNode_M7_ethernet] +build_flags = + -D ETHERNET_ENABLED + -D ETHERNET_USE_CH390 + -D ETHERNET_CLASS=CH390EthernetInterface + -D ETH_MISO_PIN=14 + -D ETH_MOSI_PIN=48 + -D ETH_SCLK_PIN=47 + -D ETH_CS_PIN=21 + -D ETH_INT_PIN=45 + -D ETHERNET_DEBUG_LOGGING=1 +build_src_filter = + + +lib_deps = + https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d + [env:ThinkNode_M7_repeater] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter} @@ -130,6 +146,26 @@ lib_deps = ${ThinkNode_M7.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:ThinkNode_M7_companion_radio_ethernet] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + ${ThinkNode_M7_ethernet.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=NullDisplayDriver + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${ThinkNode_M7.build_src_filter} + ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M7.lib_deps} + ${ThinkNode_M7_ethernet.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:ThinkNode_M7_kiss_modem] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter} From b4143a4402ebd4f0f5fbcb8bfea7cffbde74f7f7 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sat, 18 Jul 2026 00:33:35 +1200 Subject: [PATCH 160/191] rename class --- examples/companion_radio/main.cpp | 2 +- src/helpers/ethernet/{Ethernet.h => EthernetInterface.h} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/helpers/ethernet/{Ethernet.h => EthernetInterface.h} (100%) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 2fc00dbd2..b6b6d93c7 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -49,7 +49,7 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; HardwareSerial companion_serial(1); #elif defined(ETHERNET_ENABLED) - #include + #include ETHERNET_CLASS serial_interface; #else #include diff --git a/src/helpers/ethernet/Ethernet.h b/src/helpers/ethernet/EthernetInterface.h similarity index 100% rename from src/helpers/ethernet/Ethernet.h rename to src/helpers/ethernet/EthernetInterface.h From e672679d6ec5c0c849b60b9c735f386a30c0bade Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 19 Jul 2026 23:28:01 +1200 Subject: [PATCH 161/191] refactored companion interfaces to allow for multiple active connection modes --- examples/companion_radio/AbstractUITask.h | 12 +- examples/companion_radio/main.cpp | 209 ++++++++--------- examples/companion_radio/ui-new/UITask.cpp | 8 +- examples/companion_radio/ui-new/UITask.h | 4 +- examples/companion_radio/ui-orig/UITask.h | 2 +- examples/companion_radio/ui-tiny/UITask.h | 4 +- src/helpers/BaseSerialInterface.h | 1 + src/helpers/MultiSerialInterface.h | 200 ++++++++++++++++ .../ethernet/SerialEthernetInterface.cpp | 140 ++++++++++++ .../ethernet/SerialEthernetInterface.h | 75 ++++++ .../ethernet/ch390/CH390EthernetInterface.cpp | 215 +++++------------- .../ethernet/ch390/CH390EthernetInterface.h | 72 +----- variants/thinknode_m7/platformio.ini | 6 + 13 files changed, 595 insertions(+), 353 deletions(-) create mode 100644 src/helpers/MultiSerialInterface.h create mode 100644 src/helpers/ethernet/SerialEthernetInterface.cpp create mode 100644 src/helpers/ethernet/SerialEthernetInterface.h diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 0eee45aef..b25b1442f 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #ifdef PIN_BUZZER @@ -25,10 +25,10 @@ enum class UIEventType { class AbstractUITask { protected: mesh::MainBoard* _board; - BaseSerialInterface* _serial; + MultiSerialInterface* _interfaceManager; bool _connected; - AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { + AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { _connected = false; } @@ -36,9 +36,9 @@ public: void setHasConnection(bool connected) { _connected = connected; } bool hasConnection() const { return _connected; } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } - bool isSerialEnabled() const { return _serial->isEnabled(); } - void enableSerial() { _serial->enable(); } - void disableSerial() { _serial->disable(); } + bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } + void enableBluetooth() { _interfaceManager->enableBluetooth(); } + void disableBluetooth() { _interfaceManager->disableBluetooth(); } virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; virtual void notify(UIEventType t = UIEventType::none) = 0; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b6b6d93c7..7c67d90da 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,6 +12,67 @@ static uint32_t _atoi(const char* sp) { return n; } +// interface manager +#include +MultiSerialInterface interface_manager; + +// include bluetooth interface +#if defined(BLE_PIN_CODE) + #ifdef ESP32 + // include esp32 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #elif defined(NRF52_PLATFORM) + // include nrf52 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #else + #error "SerialBLEInterface is not defined for this platform" + #endif +#endif + +// include wifi interface +#ifdef WIFI_SSID + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #ifdef ESP32 + // include esp32 wifi interface + #include + SerialWifiInterface wifi_interface; + #else + #error "SerialWifiInterface is not defined for this platform" + #endif +#endif + +// include usb interface +#if defined(ENABLE_USB_INTERFACE) + #include + ArduinoSerialInterface usb_serial_interface; +#endif + +// include ethernet interface +#if defined(ETHERNET_ENABLED) + // todo refactor rak/nrf52 SerialEthernetInterface to new EthernetInterface + #if defined(NRF52_PLATFORM) + // include nrf52 ethernet interface + #include + SerialEthernetInterface ethernet_interface; + #else + // include ethernet interface + #include + ETHERNET_CLASS ethernet_interface; + #endif +#endif + +// include hardware serial interface +#if defined(SERIAL_RX) + #include + ArduinoSerialInterface hardware_serial_interface; + HardwareSerial companion_serial(1); +#endif + +// platform file system #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -34,67 +95,10 @@ static uint32_t _atoi(const char* sp) { DataStore store(SPIFFS, rtc_clock); #endif -#ifdef ESP32 - #ifdef WIFI_SSID - #include - SerialWifiInterface serial_interface; - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - #elif defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; - #elif defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #elif defined(ETHERNET_ENABLED) - #include - ETHERNET_CLASS serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(RP2040_PLATFORM) - //#ifdef WIFI_SSID - // #include - // SerialWifiInterface serial_interface; - // #ifndef TCP_PORT - // #define TCP_PORT 5000 - // #endif - // #elif defined(BLE_PIN_CODE) - // #include - // SerialBLEInterface serial_interface; - #if defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(NRF52_PLATFORM) - #ifdef BLE_PIN_CODE - #include - SerialBLEInterface serial_interface; - #elif defined(ETHERNET_ENABLED) - #include - SerialEthernetInterface serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; -#else - #error "need to define a serial interface" -#endif - /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board, &serial_interface); + UITask ui_task(&board, &interface_manager); #endif StdRNG fast_rng; @@ -164,26 +168,6 @@ void setup() { false #endif ); - -#ifdef BLE_PIN_CODE - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); - the_mesh.startInterface(serial_interface); -#elif defined(ETHERNET_ENABLED) - Serial.print("Waiting for serial to connect...\n"); - unsigned long timeout = millis(); - while (!Serial) { - if ((millis() - timeout) < 5000) { delay(100); } else { break; } - } - Serial.println("Initializing Ethernet adapter..."); - if (serial_interface.begin()) { - the_mesh.startInterface(serial_interface); - } else { - Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); - } -#else - serial_interface.begin(Serial); - the_mesh.startInterface(serial_interface); -#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); @@ -194,22 +178,6 @@ void setup() { false #endif ); - - //#ifdef WIFI_SSID - // WiFi.begin(WIFI_SSID, WIFI_PWD); - // serial_interface.begin(TCP_PORT); - // #elif defined(BLE_PIN_CODE) - // char dev_name[32+16]; - // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); - // serial_interface.begin(dev_name, the_mesh.getBLEPin()); - #if defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); - #else - serial_interface.begin(Serial); - #endif - the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); @@ -220,7 +188,17 @@ void setup() { false #endif ); +#else + #error "need to define filesystem" +#endif +// add bluetooth interface +#if defined(BLE_PIN_CODE) + bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface); +#endif + +// add wifi interface #ifdef WIFI_SSID board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -236,23 +214,31 @@ void setup() { }); WiFi.begin(WIFI_SSID, WIFI_PWD); - serial_interface.begin(TCP_PORT); -#elif defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); -#elif defined(ETHERNET_ENABLED) - serial_interface.begin(); -#else - serial_interface.begin(Serial); -#endif - the_mesh.startInterface(serial_interface); -#else - #error "need to define filesystem" + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif +// add usb interface +#if defined(ENABLE_USB_INTERFACE) + usb_serial_interface.begin(Serial); + interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); +#endif + +// add ethernet interface +#if defined(ETHERNET_ENABLED) + ethernet_interface.begin(); + interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); +#endif + +// add hardware serial interface +#if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + hardware_serial_interface.begin(companion_serial); + interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface); +#endif + + the_mesh.startInterface(interface_manager); sensors.begin(); #if ENV_INCLUDE_GPS == 1 @@ -268,6 +254,7 @@ void setup() { void loop() { the_mesh.loop(); + interface_manager.loop(); sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); @@ -277,10 +264,6 @@ void loop() { external_watchdog.loop(); #endif -#ifdef ETHERNET_ENABLED - serial_interface.loop(); -#endif - if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 50dc91d2d..051f3b31e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -289,7 +289,7 @@ public: } else if (_page == HomePage::BLUETOOTH) { display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, - _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, + _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setColor(UIColor::secondary_txt); display.setTextSize(1); @@ -449,10 +449,10 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { - if (_task->isSerialEnabled()) { // toggle Bluetooth on/off - _task->disableSerial(); + if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off + _task->disableBluetooth(); } else { - _task->enableSerial(); + _task->enableBluetooth(); } return true; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7e..52d3ffa13 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -65,7 +65,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; curr = NULL; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 60cd0d042..961c07a03 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -54,7 +54,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { _next_refresh = 0; ui_started_at = 0; } diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 344e48b98..dc689478e 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -71,7 +71,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; _cached_batt_mv = 0; ui_started_at = 0; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e9a3f2ab4..23933fcb4 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -14,6 +14,7 @@ public: virtual bool isEnabled() const = 0; virtual bool isConnected() const = 0; + virtual void loop() {}; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h new file mode 100644 index 000000000..f7742b24c --- /dev/null +++ b/src/helpers/MultiSerialInterface.h @@ -0,0 +1,200 @@ +#pragma once + +#include "BaseSerialInterface.h" + +#ifndef MAX_INTERFACES + // ble, usb, wifi, ethernet + #define MAX_INTERFACES 4 +#endif + +enum class InterfaceType : uint8_t { + NONE, + Bluetooth, + USB, + WiFi, + Ethernet, + HardwareSerial +}; + +class MultiSerialInterface : public BaseSerialInterface { +private: + + struct RegisteredInterface { + InterfaceType type = InterfaceType::NONE; + BaseSerialInterface* instance = nullptr; + }; + + bool _enabled = false; + RegisteredInterface _interfaces[MAX_INTERFACES] = {}; + +public: + bool addInterface(InterfaceType type, BaseSerialInterface* iface) { + // make sure an interface was provided + if(iface == nullptr){ + return false; + } + + // put it in the first free slot + for(int i = 0; i < MAX_INTERFACES; i++){ + if(_interfaces[i].instance == nullptr){ + _interfaces[i].instance = iface; + _interfaces[i].type = type; + return true; + } + } + + // no free slots available + return false; + } + + bool removeInterface(BaseSerialInterface* iface) { + // make sure an interface was provided + if(iface == nullptr){ + return false; + } + + // find and remove interface + for(int i = 0; i < MAX_INTERFACES; i++){ + if(_interfaces[i].instance == iface){ + _interfaces[i] = {}; + return true; + } + } + + // interface not found + return false; + } + + void enableBluetooth() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + iface.instance->enable(); + } + } + } + + void disableBluetooth() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + iface.instance->disable(); + } + } + } + + bool isBluetoothEnabled() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + return iface.instance->isEnabled(); + } + } + return false; + } + + // enable all interfaces + void enable() override { + _enabled = true; + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->enable(); + } + } + } + + // disable all interfaces + void disable() override { + _enabled = false; + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->disable(); + } + } + } + + bool isEnabled() const override { + return _enabled; + } + + bool isConnected() const override { + // not connected when disabled + if(!_enabled){ + return false; + } + + // check if any interface is connected + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isConnected()) { + return true; + } + } + + // nothing connected + return false; + } + + // loop all interfaces + void loop() override { + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->loop(); + } + } + } + + bool isWriteBusy() const override { + // not busy when disabled + if(!_enabled){ + return false; + } + + // check if any interface is busy + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled() && iface.instance->isWriteBusy()){ + return true; + } + } + + // nothing busy + return false; + } + + size_t writeFrame(const uint8_t src[], size_t len) override { + // don't write when disabled or nothing provided + if(!_enabled || len == 0){ + return 0; + } + + // write frame to all enabled interfaces + bool allSuccessful = true; + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled()){ + if(iface.instance->writeFrame(src, len) != len){ + allSuccessful = false; + } + } + } + + // report success if all writes completed successfully + return allSuccessful ? len : 0; + } + + size_t checkRecvFrame(uint8_t dest[]) override { + // don't read when disabled + if(!_enabled){ + return 0; + } + + // try to read a frame from any enabled interface + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled()){ + size_t frameSize = iface.instance->checkRecvFrame(dest); + if(frameSize > 0){ + return frameSize; + } + } + } + + // no frame received + return 0; + } + +}; diff --git a/src/helpers/ethernet/SerialEthernetInterface.cpp b/src/helpers/ethernet/SerialEthernetInterface.cpp new file mode 100644 index 000000000..22bcabc10 --- /dev/null +++ b/src/helpers/ethernet/SerialEthernetInterface.cpp @@ -0,0 +1,140 @@ +#include "SerialEthernetInterface.h" + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool SerialEthernetInterface::begin() { + return true; +} + +void SerialEthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); +} + +void SerialEthernetInterface::disable() { + _isEnabled = false; +} + +size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (isConnected() && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool SerialEthernetInterface::isWriteBusy() const { + return false; +} + +void SerialEthernetInterface::onClientConnected() { + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; +} + +size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { + + if (isConnected()) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (available()) { + int c = read(); + if (c < 0) break; + +#if ETHERNET_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} diff --git a/src/helpers/ethernet/SerialEthernetInterface.h b/src/helpers/ethernet/SerialEthernetInterface.h new file mode 100644 index 000000000..46789f710 --- /dev/null +++ b/src/helpers/ethernet/SerialEthernetInterface.h @@ -0,0 +1,75 @@ +#pragma once + +#include "../BaseSerialInterface.h" + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class SerialEthernetInterface : public BaseSerialInterface { + bool _isEnabled; + unsigned long _last_write; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + SerialEthernetInterface() { + _isEnabled = false; + _last_write = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + + void onClientConnected(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + + virtual int available() = 0; + virtual int read() = 0; + virtual size_t write(const uint8_t *buf, size_t size) = 0; +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} +#endif diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp index f7ee55030..7f6962453 100644 --- a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp @@ -1,12 +1,33 @@ #include "CH390EthernetInterface.h" -#define RECV_STATE_IDLE 0 -#define RECV_STATE_HDR_FOUND 1 -#define RECV_STATE_LEN1_FOUND 2 -#define RECV_STATE_LEN2_FOUND 3 +void onWiFiEvent(WiFiEvent_t event) { + switch(event){ + case ARDUINO_EVENT_ETH_START: + ETHERNET_DEBUG_PRINTLN("Ethernet Started"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + ETHERNET_DEBUG_PRINTLN("Ethernet Connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + ETHERNET_DEBUG_PRINTLN("Ethernet Disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + ETHERNET_DEBUG_PRINTLN("Ethernet Got IP"); + ETHERNET_DEBUG_PRINT_IP("IP Address", CH390.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet Mask", CH390.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", CH390.gatewayIP()); + ETHERNET_DEBUG_PRINT_IP("DNS", CH390.dnsIP()); + ETHERNET_DEBUG_PRINTLN("MAC Address: %s", CH390.macAddress().c_str()); + break; + default: + break; + } +} bool CH390EthernetInterface::begin() { - ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + + // listen to ethernet events + WiFi.onEvent(onWiFiEvent); // Init CH390 ch390_config_t config = CH390_DEFAULT_CONFIG(); @@ -29,179 +50,45 @@ bool CH390EthernetInterface::begin() { #endif // Start Server - server.begin(); + server.begin(ETHERNET_TCP_PORT); ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); return true; } -void CH390EthernetInterface::enable() { - if (_isEnabled) return; - _isEnabled = true; - clearBuffers(); +int CH390EthernetInterface::available() { + return client.available(); } -void CH390EthernetInterface::disable() { - _isEnabled = false; +int CH390EthernetInterface::read() { + return client.read(); } -size_t CH390EthernetInterface::writeFrame(const uint8_t src[], size_t len) { - if (len > MAX_FRAME_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); - return 0; - } - - if (deviceConnected && len > 0) { - if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); - return 0; - } - - send_queue[send_queue_len].len = len; // add to send queue - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; - - return len; - } - return 0; -} - -bool CH390EthernetInterface::isWriteBusy() const { - return false; -} - -size_t CH390EthernetInterface::checkRecvFrame(uint8_t dest[]) { - if (server.hasClient()) { - auto newClient = server.available(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "New client accepted %u.%u.%u.%u:%u", - new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); - } - } - - if (client.connected()) { - if (!deviceConnected) { - ETHERNET_DEBUG_PRINTLN( - "Got connection %u.%u.%u.%u:%u", - client.remoteIP()[0], - client.remoteIP()[1], - client.remoteIP()[2], - client.remoteIP()[3], - client.remotePort()); - deviceConnected = true; - } - } else { - if (deviceConnected) { - deviceConnected = false; - ETHERNET_DEBUG_PRINTLN("Disconnected"); - } - } - - if (deviceConnected) { - if (send_queue_len > 0) { // first, check send queue - - _last_write = millis(); - int len = send_queue[0].len; - -#if ETHERNET_RAW_LINE - ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); - client.write(send_queue[0].buf, len); - client.write("\r\n", 2); -#else - uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames - pkt[0] = '>'; - pkt[1] = (len & 0xFF); // LSB - pkt[2] = (len >> 8); // MSB - memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); - #endif - client.write(pkt, 3 + len); -#endif - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) { // delete top item from queue - send_queue[i] = send_queue[i + 1]; - } - } else { - while (client.available()) { - int c = client.read(); - if (c < 0) break; - -#if ETHERNET_RAW_LINE - if (c == '\r' || c == '\n') { - if (_rx_len == 0) { - continue; - } - uint16_t out_len = _rx_len; - if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; - memcpy(dest, _rx_buf, out_len); - _rx_len = 0; - return out_len; - } - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - _rx_len++; - } -#else - switch (_state) { - case RECV_STATE_IDLE: - if (c == '<') { - _state = RECV_STATE_HDR_FOUND; - } - break; - case RECV_STATE_HDR_FOUND: - _frame_len = (uint8_t)c; - _state = RECV_STATE_LEN1_FOUND; - break; - case RECV_STATE_LEN1_FOUND: - _frame_len |= ((uint16_t)c) << 8; - _rx_len = 0; - _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; - break; - default: - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - } - _rx_len++; - if (_rx_len >= _frame_len) { - if (_frame_len > MAX_FRAME_SIZE) { - _frame_len = MAX_FRAME_SIZE; - } - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); - #endif - memcpy(dest, _rx_buf, _frame_len); - _state = RECV_STATE_IDLE; - return _frame_len; - } - } -#endif - } - } - } - - return 0; +size_t CH390EthernetInterface::write(const uint8_t *buf, size_t size) { + return client.write(buf, size); } bool CH390EthernetInterface::isConnected() const { - return deviceConnected; + return _isConnected; } void CH390EthernetInterface::loop() { + if (server.hasClient()) { + auto newClient = server.available(); + if (newClient) { + IPAddress remoteIp = newClient.remoteIP(); + uint16_t remotePort = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + client = newClient; + onClientConnected(); + } + } + + _isConnected = client.connected(); + } diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.h b/src/helpers/ethernet/ch390/CH390EthernetInterface.h index d638319bc..09c3fc23d 100644 --- a/src/helpers/ethernet/ch390/CH390EthernetInterface.h +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.h @@ -1,80 +1,30 @@ #pragma once -#include "../../BaseSerialInterface.h" +#include "../SerialEthernetInterface.h" #include #include #include #include #include -#ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 5000 -#endif -// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets - -class CH390EthernetInterface : public BaseSerialInterface { - bool deviceConnected; - bool _isEnabled; - unsigned long _last_write; - uint8_t _state; - uint16_t _frame_len; - uint16_t _rx_len; - uint8_t _rx_buf[MAX_FRAME_SIZE]; - +class CH390EthernetInterface : public SerialEthernetInterface { + + bool _isConnected; WiFiServer server; WiFiClient client; - struct Frame { - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - - #define FRAME_QUEUE_SIZE 4 - int send_queue_len; - Frame send_queue[FRAME_QUEUE_SIZE]; - - void clearBuffers() { - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - - protected: - public: - CH390EthernetInterface() : server(ETHERNET_TCP_PORT) { - deviceConnected = false; - _isEnabled = false; - _last_write = 0; - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; + CH390EthernetInterface(){ + _isConnected = false; } + bool begin(); - void loop(); + void loop() override; // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } - bool isConnected() const override; - bool isWriteBusy() const override; - size_t writeFrame(const uint8_t src[], size_t len) override; - size_t checkRecvFrame(uint8_t dest[]) override; + int available() override; + int read() override; + size_t write(const uint8_t *buf, size_t size) override; }; - - -#if ETHERNET_DEBUG_LOGGING && ARDUINO - #include - #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) -#else - #define ETHERNET_DEBUG_PRINT(...) {} - #define ETHERNET_DEBUG_PRINTLN(...) {} - #define ETHERNET_DEBUG_PRINT_IP(...) {} -#endif diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 43ff61c3a..2ff6fc0ab 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -1,6 +1,8 @@ [ThinkNode_M7] extends = esp32_base board = thinknode_m7 +board_upload.flash_size = 8MB +board_build.partitions = default_8MB.csv build_flags = ${esp32_base.build_flags} -I src/helpers/esp32 -I variants/thinknode_m7 @@ -46,6 +48,7 @@ build_flags = -D ETH_INT_PIN=45 -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = + + + lib_deps = https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d @@ -88,6 +91,7 @@ lib_deps = extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} + ${ThinkNode_M7_ethernet.build_flags} -I examples/companion_radio/ui-orig -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 @@ -97,6 +101,7 @@ build_flags = ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + ${ThinkNode_M7_ethernet.build_src_filter} + + + @@ -104,6 +109,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} + ${ThinkNode_M7_ethernet.lib_deps} densaugeo/base64 @ ~1.4.0 [env:ThinkNode_M7_companion_radio_usb] From 203fd4e407ef17f22518734bc35ffacbab43f64d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:05:49 +1200 Subject: [PATCH 162/191] refactor rak13800 to new ethernet interface class --- examples/companion_radio/main.cpp | 12 +- src/helpers/ethernet/EthernetInterface.h | 2 + .../RAK13800/RAK13800EthernetInterface.cpp | 109 ++++++++++++++++++ .../RAK13800/RAK13800EthernetInterface.h | 27 +++++ variants/rak4631/platformio.ini | 5 +- 5 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp create mode 100644 src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 7c67d90da..89f0e6cb9 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -53,16 +53,8 @@ MultiSerialInterface interface_manager; // include ethernet interface #if defined(ETHERNET_ENABLED) - // todo refactor rak/nrf52 SerialEthernetInterface to new EthernetInterface - #if defined(NRF52_PLATFORM) - // include nrf52 ethernet interface - #include - SerialEthernetInterface ethernet_interface; - #else - // include ethernet interface - #include - ETHERNET_CLASS ethernet_interface; - #endif + #include + ETHERNET_CLASS ethernet_interface; #endif // include hardware serial interface diff --git a/src/helpers/ethernet/EthernetInterface.h b/src/helpers/ethernet/EthernetInterface.h index 4d889df45..4e1f9176a 100644 --- a/src/helpers/ethernet/EthernetInterface.h +++ b/src/helpers/ethernet/EthernetInterface.h @@ -3,6 +3,8 @@ #if defined(ETHERNET_ENABLED) #if defined(ETHERNET_USE_CH390) #include "helpers/ethernet/ch390/CH390EthernetInterface.h" + #elif defined(ETHERNET_USE_RAK13800) + #include "helpers/ethernet/RAK13800/RAK13800EthernetInterface.h" #else #error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!" #endif diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp new file mode 100644 index 000000000..027581e30 --- /dev/null +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -0,0 +1,109 @@ +#include "RAK13800EthernetInterface.h" +#include "../../nrf52/EthernetMac.h" +#include +#include + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 + +bool RAK13800EthernetInterface::begin() { + + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, HIGH); +#endif + + // generate mac address + uint8_t mac[6]; + generateEthernetMac(mac); + ETHERNET_DEBUG_PRINTLN( + "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); + + // Use static IP if build flags are defined, otherwise DHCP + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gateway(ETHERNET_STATIC_GATEWAY); + IPAddress subnet(ETHERNET_STATIC_SUBNET); + IPAddress dns(ETHERNET_STATIC_DNS); + Ethernet.begin(mac, ip, dns, gateway, subnet); + #else + if (Ethernet.begin(mac) == 0) { + ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware."); + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); + } else if (Ethernet.linkStatus() == LinkOFF) { + ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); + } else { + ETHERNET_DEBUG_PRINTLN("DHCP failed for unknown reason."); + } + return false; + } + #endif + + ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); + ETHERNET_DEBUG_PRINT_IP("IP Address", Ethernet.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet Mask", Ethernet.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); + ETHERNET_DEBUG_PRINT_IP("DNS", Ethernet.dnsServerIP()); + + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + + return true; +} + +int RAK13800EthernetInterface::available() { + return client.available(); +} + +int RAK13800EthernetInterface::read() { + return client.read(); +} + +size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) { + return client.write(buf, size); +} + +bool RAK13800EthernetInterface::isConnected() const { + return _isConnected; +} + +void RAK13800EthernetInterface::loop() { + + Ethernet.maintain(); + + auto newClient = server.accept(); + if (newClient) { + IPAddress remoteIp = newClient.remoteIP(); + uint16_t remotePort = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + client = newClient; + onClientConnected(); + } + + _isConnected = client.connected(); + +} diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h new file mode 100644 index 000000000..34bb75877 --- /dev/null +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../SerialEthernetInterface.h" +#include +#include + +class RAK13800EthernetInterface : public SerialEthernetInterface { + + bool _isConnected; + EthernetServer server; + EthernetClient client; + + public: + RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { + _isConnected = false; + } + + bool begin(); + void loop() override; + + // BaseSerialInterface methods + bool isConnected() const override; + + int available() override; + int read() override; + size_t write(const uint8_t *buf, size_t size) override; +}; diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 31b507b4c..65c7ce54e 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -190,13 +190,16 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D ETHERNET_ENABLED=1 + -D ETHERNET_USE_RAK13800 + -D ETHERNET_CLASS=RAK13800EthernetInterface ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 ; -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> - + + + + + lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 From 834ad722a4f8a685e09e43e349a15a5cfe9caf17 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:16:59 +1200 Subject: [PATCH 163/191] remove old rak ethernet interface --- src/helpers/nrf52/SerialEthernetInterface.cpp | 268 ------------------ src/helpers/nrf52/SerialEthernetInterface.h | 82 ------ 2 files changed, 350 deletions(-) delete mode 100644 src/helpers/nrf52/SerialEthernetInterface.cpp delete mode 100644 src/helpers/nrf52/SerialEthernetInterface.h diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp deleted file mode 100644 index 36a998a4e..000000000 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#ifdef ETHERNET_ENABLED - -#include "SerialEthernetInterface.h" -#include "EthernetMac.h" -#include -#include - -#define PIN_SPI1_MISO (29) // (0 + 29) -#define PIN_SPI1_MOSI (30) // (0 + 30) -#define PIN_SPI1_SCK (3) // (0 + 3) - -SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - -#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable -#define PIN_ETHERNET_RESET 21 -#define PIN_ETHERNET_SS 26 - -#define RECV_STATE_IDLE 0 -#define RECV_STATE_HDR_FOUND 1 -#define RECV_STATE_LEN1_FOUND 2 -#define RECV_STATE_LEN2_FOUND 3 - -bool SerialEthernetInterface::begin() { - - ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); - - // WB_IO2 (power enable) is already driven HIGH by early constructor - // in RAK4631Board.cpp to support POE boot. - // Skip hardware reset — the W5100S comes out of power-on reset cleanly, - // and toggling reset kills the PHY link which breaks POE power. -#ifdef PIN_ETHERNET_RESET - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, HIGH); -#endif - - uint8_t mac[6]; - generateEthernetMac(mac); - ETHERNET_DEBUG_PRINTLN( - "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", - mac[0], - mac[1], - mac[2], - mac[3], - mac[4], - mac[5]); - ETHERNET_DEBUG_PRINTLN("Init"); - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - // Use static IP if build flags are defined, otherwise DHCP - #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) - IPAddress ip(ETHERNET_STATIC_IP); - IPAddress gateway(ETHERNET_STATIC_GATEWAY); - IPAddress subnet(ETHERNET_STATIC_SUBNET); - IPAddress dns(ETHERNET_STATIC_DNS); - Ethernet.begin(mac, ip, dns, gateway, subnet); - #else - ETHERNET_DEBUG_PRINTLN("Begin"); - if (Ethernet.begin(mac) == 0) { - ETHERNET_DEBUG_PRINTLN("Begin failed."); - - // DHCP failed -- let's figure out why - if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. - { - ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); - return false; - } - if (Ethernet.linkStatus() == LinkOFF) // No physical connection - { - ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); - return false; - } - ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); - return false; - } - #endif - ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); - ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP()); - ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask()); - ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); - - server.begin(); // start listening for clients - ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); - - return true; -} - -void SerialEthernetInterface::enable() { - if (_isEnabled) return; - - _isEnabled = true; - clearBuffers(); -} - -void SerialEthernetInterface::disable() { - _isEnabled = false; -} - -size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { - if (len > MAX_FRAME_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); - return 0; - } - - if (deviceConnected && len > 0) { - if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); - return 0; - } - - send_queue[send_queue_len].len = len; // add to send queue - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; - - return len; - } - return 0; -} - -bool SerialEthernetInterface::isWriteBusy() const { - return false; -} - -size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // Use accept() (not available()) so we only see newly-accepted sockets. - // available() also returns existing connected sockets that have data, - // which would cause us to treat each inbound packet as a "new client" - // and stop() the underlying socket — disconnecting the companion. - auto newClient = server.accept(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "New client accepted %u.%u.%u.%u:%u", - new_ip[0], - new_ip[1], - new_ip[2], - new_ip[3], - new_port); - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); - } - - if (client.connected()) { - if (!deviceConnected) { - ETHERNET_DEBUG_PRINTLN( - "Got connection %u.%u.%u.%u:%u", - client.remoteIP()[0], - client.remoteIP()[1], - client.remoteIP()[2], - client.remoteIP()[3], - client.remotePort()); - deviceConnected = true; - } - } else { - if (deviceConnected) { - deviceConnected = false; - ETHERNET_DEBUG_PRINTLN("Disconnected"); - } - } - - if (deviceConnected) { - if (send_queue_len > 0) { // first, check send queue - - _last_write = millis(); - int len = send_queue[0].len; - -#if ETHERNET_RAW_LINE - ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); - client.write(send_queue[0].buf, len); - client.write("\r\n", 2); -#else - uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames - pkt[0] = '>'; - pkt[1] = (len & 0xFF); // LSB - pkt[2] = (len >> 8); // MSB - memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); - #endif - client.write(pkt, 3 + len); -#endif - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) { // delete top item from queue - send_queue[i] = send_queue[i + 1]; - } - } else { - while (client.available()) { - int c = client.read(); - if (c < 0) break; - -#if ETHERNET_RAW_LINE - if (c == '\r' || c == '\n') { - if (_rx_len == 0) { - continue; - } - uint16_t out_len = _rx_len; - if (out_len > MAX_FRAME_SIZE) { - out_len = MAX_FRAME_SIZE; - } - memcpy(dest, _rx_buf, out_len); - _rx_len = 0; - return out_len; - } - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - _rx_len++; - } -#else - switch (_state) { - case RECV_STATE_IDLE: - if (c == '<') { - _state = RECV_STATE_HDR_FOUND; - } - break; - case RECV_STATE_HDR_FOUND: - _frame_len = (uint8_t)c; - _state = RECV_STATE_LEN1_FOUND; - break; - case RECV_STATE_LEN1_FOUND: - _frame_len |= ((uint16_t)c) << 8; - _rx_len = 0; - _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; - break; - default: - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - } - _rx_len++; - if (_rx_len >= _frame_len) { - if (_frame_len > MAX_FRAME_SIZE) { - _frame_len = MAX_FRAME_SIZE; - } - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); - #endif - memcpy(dest, _rx_buf, _frame_len); - _state = RECV_STATE_IDLE; - return _frame_len; - } - } -#endif - } - } - } - - return 0; -} - -bool SerialEthernetInterface::isConnected() const { - return deviceConnected; -} - -void SerialEthernetInterface::loop() { - Ethernet.maintain(); -} - -#endif // ETHERNET_ENABLED diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h deleted file mode 100644 index b8b4e94b2..000000000 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#ifdef ETHERNET_ENABLED - -#include "helpers/BaseSerialInterface.h" -#include -#include - -#ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 5000 -#endif -// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets - -class SerialEthernetInterface : public BaseSerialInterface { - bool deviceConnected; - bool _isEnabled; - unsigned long _last_write; - uint8_t _state; - uint16_t _frame_len; - uint16_t _rx_len; - uint8_t _rx_buf[MAX_FRAME_SIZE]; - - EthernetServer server; - EthernetClient client; - - struct Frame { - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - - #define FRAME_QUEUE_SIZE 4 - int send_queue_len; - Frame send_queue[FRAME_QUEUE_SIZE]; - - void clearBuffers() { - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - - protected: - - public: - SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { - deviceConnected = false; - _isEnabled = false; - _last_write = 0; - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - bool begin(); - - // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } - - bool isConnected() const override; - bool isWriteBusy() const override; - - size_t writeFrame(const uint8_t src[], size_t len) override; - size_t checkRecvFrame(uint8_t dest[]) override; - - void loop(); -}; - - -#if ETHERNET_DEBUG_LOGGING && ARDUINO - #include - #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) -#else - #define ETHERNET_DEBUG_PRINT(...) {} - #define ETHERNET_DEBUG_PRINTLN(...) {} - #define ETHERNET_DEBUG_PRINT_IP(...) {} -#endif - -#endif // ETHERNET_ENABLED From e7f2c71701f5e5f42507440d05f8622dc2a9430d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:45:02 +1200 Subject: [PATCH 164/191] usb companion firmwares now require build flag to include usb interface --- variants/ebyte_eora_s3/platformio.ini | 1 + variants/gat562_30s_mesh_kit/platformio.ini | 1 + variants/gat562_mesh_tracker_pro/platformio.ini | 1 + variants/heltec_ct62/platformio.ini | 1 + variants/heltec_e213/platformio.ini | 1 + variants/heltec_mesh_solar/platformio.ini | 1 + variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_t096/platformio.ini | 1 + variants/heltec_t1/platformio.ini | 1 + variants/heltec_t114/platformio.ini | 2 ++ variants/heltec_t190/platformio.ini | 1 + variants/heltec_tower_v2/platformio.ini | 1 + variants/heltec_tracker/platformio.ini | 1 + variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/heltec_wireless_paper/platformio.ini | 1 + variants/ikoka_handheld_nrf/platformio.ini | 2 ++ variants/ikoka_nano_nrf/platformio.ini | 1 + variants/ikoka_stick_nrf/platformio.ini | 1 + variants/keepteen_lt1/platformio.ini | 1 + variants/lilygo_t3s3/platformio.ini | 1 + variants/lilygo_t3s3_sx1276/platformio.ini | 1 + variants/lilygo_tbeam_1w/platformio.ini | 1 + variants/lilygo_tdeck/platformio.ini | 1 + variants/lilygo_techo/platformio.ini | 1 + variants/lilygo_techo_card/platformio.ini | 1 + variants/lilygo_techo_lite/platformio.ini | 1 + variants/lilygo_teth_elite/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/m5stack_unit_c6l/platformio.ini | 1 + variants/mesh_pocket/platformio.ini | 1 + variants/meshadventurer/platformio.ini | 2 ++ variants/meshtiny/platformio.ini | 1 + variants/minewsemi_me25ls01/platformio.ini | 1 + variants/muziworks_r1_neo/platformio.ini | 1 + variants/nano_g2_ultra/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/promicro/platformio.ini | 1 + variants/rak11310/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rak3401/platformio.ini | 1 + variants/rak3x72/platformio.ini | 1 + variants/rak4631/platformio.ini | 1 + variants/rak_wismesh_tag/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 1 + variants/sensecap_solar/platformio.ini | 1 + variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/t1000-e/platformio.ini | 1 + variants/thinknode_m1/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m3/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m6/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/tiny_relay/platformio.ini | 1 + variants/waveshare_rp2040_lora/platformio.ini | 1 + variants/wio-e5-dev/platformio.ini | 1 + variants/wio-e5-mini/platformio.ini | 1 + variants/wio-tracker-l1/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_nrf52/platformio.ini | 1 + variants/xiao_rp2040/platformio.ini | 1 + variants/xiao_s3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 70 files changed, 77 insertions(+) diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini index 15fe761ba..1ab3d3fb1 100644 --- a/variants/ebyte_eora_s3/platformio.ini +++ b/variants/ebyte_eora_s3/platformio.ini @@ -102,6 +102,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 2baac2561..89276a49a 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -75,6 +75,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter} diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index af153b8fc..142cfe4b8 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -71,6 +71,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter} diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 0179d9658..401a30d23 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -101,6 +101,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_ct62.build_src_filter} diff --git a/variants/heltec_e213/platformio.ini b/variants/heltec_e213/platformio.ini index 123edd91c..021938190 100644 --- a/variants/heltec_e213/platformio.ini +++ b/variants/heltec_e213/platformio.ini @@ -73,6 +73,7 @@ build_flags = -D DISPLAY_CLASS=E213Display -D AUTO_OFF_MILLIS=0 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_E213_base.build_src_filter} + + diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index fb5cd5152..17f8ba217 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -87,6 +87,7 @@ build_flags = ${Heltec_mesh_solar.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 6361f61de..354004f07 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -131,6 +131,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_RC32.build_src_filter} + + @@ -275,6 +276,7 @@ build_flags = -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 0a062f00b..02e883287 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -152,6 +152,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index eaa8af378..28ed67edb 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -90,6 +90,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t1.build_src_filter} diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 48dfe0781..48fb4b0f5 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -127,6 +127,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_t114.build_src_filter} + + @@ -238,6 +239,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_t190/platformio.ini b/variants/heltec_t190/platformio.ini index 411fee851..283553937 100644 --- a/variants/heltec_t190/platformio.ini +++ b/variants/heltec_t190/platformio.ini @@ -74,6 +74,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_T190_base.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index 9c703ae1c..207e0a508 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -87,6 +87,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 07d2e987a..75d1cadf3 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -59,6 +59,7 @@ build_flags = -D DISPLAY_CLASS=ST7735Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; HWT will use display for pin ; -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index f1fe9dd05..d040b72f9 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_tracker_v2.build_src_filter} diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index ba4f86942..78561a14a 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -137,6 +137,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a70a93a50..65e636eb4 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -144,6 +144,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -326,6 +327,7 @@ build_flags = ${Heltec_lora32_v3.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index cb76beb3d..d718c006c 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -187,6 +187,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_oled.build_src_filter} @@ -351,6 +352,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7789LCDDisplay + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_tft.build_src_filter} diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 4057d6f1b..f523ebf9b 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -138,6 +138,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + @@ -262,6 +263,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7789LCDDisplay + -D ENABLE_USB_INTERFACE build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 48723d169..55ed62b34 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -73,6 +73,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=E213Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + diff --git a/variants/ikoka_handheld_nrf/platformio.ini b/variants/ikoka_handheld_nrf/platformio.ini index 51b602e40..1c6f17c24 100644 --- a/variants/ikoka_handheld_nrf/platformio.ini +++ b/variants/ikoka_handheld_nrf/platformio.ini @@ -74,6 +74,7 @@ extends = ikoka_handheld_nrf board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} -D LORA_TX_POWER=20 + -D ENABLE_USB_INTERFACE build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb] @@ -82,6 +83,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} -D LORA_TX_POWER=20 -D DISPLAY_ROTATION=2 + -D ENABLE_USB_INTERFACE build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} [env:ikoka_handheld_nrf_e22_30dbm_repeater] diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index 7880ea4a3..87c6240ee 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -105,6 +105,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_nano_nrf.build_src_filter} diff --git a/variants/ikoka_stick_nrf/platformio.ini b/variants/ikoka_stick_nrf/platformio.ini index 06e39e84c..c8184fc40 100644 --- a/variants/ikoka_stick_nrf/platformio.ini +++ b/variants/ikoka_stick_nrf/platformio.ini @@ -111,6 +111,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_stick_nrf.build_src_filter} diff --git a/variants/keepteen_lt1/platformio.ini b/variants/keepteen_lt1/platformio.ini index 27cf809e0..11dc214b0 100644 --- a/variants/keepteen_lt1/platformio.ini +++ b/variants/keepteen_lt1/platformio.ini @@ -66,6 +66,7 @@ build_flags = ${KeepteenLT1.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${KeepteenLT1.build_src_filter} diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index 54990117c..966235c66 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index e579e91ca..c6497dcb1 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -138,6 +138,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index c7a595520..0f604fac4 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -114,6 +114,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D PERSISTANT_GPS=1 -D ENV_SKIP_GPS_DETECT=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} diff --git a/variants/lilygo_tdeck/platformio.ini b/variants/lilygo_tdeck/platformio.ini index 745d8ff53..00571db93 100644 --- a/variants/lilygo_tdeck/platformio.ini +++ b/variants/lilygo_tdeck/platformio.ini @@ -71,6 +71,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_TDeck.build_src_filter} + + diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 5df77f95c..81da2e48c 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D UI_SENSORS_PAGE=1 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T-Echo.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index c7bb73785..4ebf43da9 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -110,6 +110,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-tiny/*.cpp> diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index a9b3d124d..9ec73d59a 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -174,6 +174,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D UI_RECENT_LIST_SIZE=9 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/lilygo_teth_elite/platformio.ini b/variants/lilygo_teth_elite/platformio.ini index 97728f8b4..ee1b98795 100644 --- a/variants/lilygo_teth_elite/platformio.ini +++ b/variants/lilygo_teth_elite/platformio.ini @@ -72,6 +72,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 367316686..1aea74d28 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -77,6 +77,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index 94083eb48..190add043 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -97,6 +97,7 @@ build_flags = ${M5Stack_Unit_C6L.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 + -D ENABLE_USB_INTERFACE build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} + - diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index 0d2a74adf..a6f823dbf 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -96,6 +96,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D AUTO_OFF_MILLIS=0 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/meshadventurer/platformio.ini b/variants/meshadventurer/platformio.ini index f85be2388..149d324f9 100644 --- a/variants/meshadventurer/platformio.ini +++ b/variants/meshadventurer/platformio.ini @@ -186,6 +186,7 @@ build_flags = -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=128 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = @@ -266,6 +267,7 @@ build_flags = -D MAX_CONTACTS=160 -D OFFLINE_QUEUE_SIZE=128 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = diff --git a/variants/meshtiny/platformio.ini b/variants/meshtiny/platformio.ini index c5439c88f..c0a3c5f94 100644 --- a/variants/meshtiny/platformio.ini +++ b/variants/meshtiny/platformio.ini @@ -36,6 +36,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Meshtiny.build_src_filter} diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index ac81fa16b..b1d3a161a 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -144,6 +144,7 @@ build_flags = ${me25ls01.build_flags} -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE -D DISPLAY_CLASS=NullDisplayDriver + -D ENABLE_USB_INTERFACE build_src_filter = ${me25ls01.build_src_filter} + + diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 3dbecf1e8..cf02b9e2c 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -66,6 +66,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${R1Neo.build_src_filter} diff --git a/variants/nano_g2_ultra/platformio.ini b/variants/nano_g2_ultra/platformio.ini index 3cdc29ffb..b817b3e99 100644 --- a/variants/nano_g2_ultra/platformio.ini +++ b/variants/nano_g2_ultra/platformio.ini @@ -99,6 +99,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D PIN_BUZZER=4 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Nano_G2_Ultra.build_src_filter} diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 6a2f3dec2..112181df2 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -107,6 +107,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 83495cd83..1161743ea 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -104,6 +104,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e1586..dea4ccfae 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -119,6 +119,7 @@ build_flags = ${Promicro.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Promicro.build_src_filter} diff --git a/variants/rak11310/platformio.ini b/variants/rak11310/platformio.ini index ab820cf6a..c8526317a 100644 --- a/variants/rak11310/platformio.ini +++ b/variants/rak11310/platformio.ini @@ -85,6 +85,7 @@ extends = rak11310 build_flags = ${rak11310.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak11310.build_src_filter} diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 9cd32c4b4..8bd3c5977 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -136,6 +136,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548b..48e719226 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -67,6 +67,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak3401.build_src_filter} diff --git a/variants/rak3x72/platformio.ini b/variants/rak3x72/platformio.ini index f96678608..c23019c80 100644 --- a/variants/rak3x72/platformio.ini +++ b/variants/rak3x72/platformio.ini @@ -37,6 +37,7 @@ build_flags = ${rak3x72.build_flags} ; -D FORMAT_FS=true -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${rak3x72.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${rak3x72.lib_deps} diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 65c7ce54e..1a6109566 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -165,6 +165,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index e9cddb74d..d61401871 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -71,6 +71,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak_wismesh_tag.build_src_filter} diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 66c7b9f89..0fe8c4369 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -58,6 +58,7 @@ extends = rpi_picow build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index effef38cc..6e0eadbcb 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -92,6 +92,7 @@ build_flags = ${SenseCap_Solar.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${SenseCap_Solar.build_src_filter} diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 508ffe4b5..e463aee0f 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -188,6 +188,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index b29388ef7..96eca1fcf 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -103,6 +103,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 6f32e54d0..8456dc915 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -82,6 +82,7 @@ build_flags = ${t1000-e.build_flags} -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=25 -D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E + -D ENABLE_USB_INTERFACE build_src_filter = ${t1000-e.build_src_filter} + + diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 617f92405..89c48403b 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -117,6 +117,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=6 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M1.build_src_filter} + + diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index aae83324e..79dac6ab8 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -159,6 +159,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M2.build_src_filter} + + diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index f80a61618..74382b36c 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -83,6 +83,7 @@ build_flags = ${ThinkNode_M3.build_flags} -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=23 -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M3.build_src_filter} + + diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 8572d1ebe..eda21bf0d 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -173,6 +173,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M5.build_src_filter} + + diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 187a8819f..3606d6a87 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -110,6 +110,7 @@ build_flags = -D QSPIFLASH=1 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M6.build_src_filter} + + diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 2ff6fc0ab..eb745b6d1 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M7.build_src_filter} + + diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index a7171726a..09b1391d6 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M9.build_src_filter} + + diff --git a/variants/tiny_relay/platformio.ini b/variants/tiny_relay/platformio.ini index 82cb251fd..13c2ec1cb 100644 --- a/variants/tiny_relay/platformio.ini +++ b/variants/tiny_relay/platformio.ini @@ -44,6 +44,7 @@ build_flags = ${Tiny_Relay.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D MAX_LORA_TX_POWER=22 + -D ENABLE_USB_INTERFACE build_src_filter = ${Tiny_Relay.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${Tiny_Relay.lib_deps} diff --git a/variants/waveshare_rp2040_lora/platformio.ini b/variants/waveshare_rp2040_lora/platformio.ini index 7dfe14012..db1ec6038 100644 --- a/variants/waveshare_rp2040_lora/platformio.ini +++ b/variants/waveshare_rp2040_lora/platformio.ini @@ -84,6 +84,7 @@ extends = waveshare_rp2040_lora build_flags = ${waveshare_rp2040_lora.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${waveshare_rp2040_lora.build_src_filter} diff --git a/variants/wio-e5-dev/platformio.ini b/variants/wio-e5-dev/platformio.ini index 22bdc3c83..82b3781e3 100644 --- a/variants/wio-e5-dev/platformio.ini +++ b/variants/wio-e5-dev/platformio.ini @@ -46,6 +46,7 @@ build_flags = ${lora_e5.build_flags} -D LORA_TX_POWER=22 -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${lora_e5.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${lora_e5.lib_deps} diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 7cf261987..021c7db20 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -44,6 +44,7 @@ build_flags = ${lora_e5_mini.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=NullDisplayDriver + -D ENABLE_USB_INTERFACE build_src_filter = ${lora_e5_mini.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 7bb175bb9..fc958ea2d 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -68,6 +68,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${WioTrackerL1.build_src_filter} diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c0e8458d0..587c5c273 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -100,6 +100,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a08543368..4551a2334 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -74,6 +74,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini index ca00e38b5..63c6798de 100644 --- a/variants/xiao_rp2040/platformio.ini +++ b/variants/xiao_rp2040/platformio.ini @@ -61,6 +61,7 @@ extends = Xiao_rp2040 build_flags = ${Xiao_rp2040.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Xiao_rp2040.build_src_filter} diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 22464e7d8..1632d1120 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -123,6 +123,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index db8c5a948..293a13c15 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -136,6 +136,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} From dd778e2189cf4fe6e3ba4623aea7d2c239ff2363 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:50:08 +1200 Subject: [PATCH 165/191] fix bluetooth toggle on ui tiny --- examples/companion_radio/ui-tiny/UITask.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 5b9ebfd39..b6bdbcf4b 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -240,7 +240,7 @@ public: } else if (_page == HomePage::BLUETOOTH) { display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, - _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, + _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); @@ -391,10 +391,10 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { - if (_task->isSerialEnabled()) { // toggle Bluetooth on/off - _task->disableSerial(); + if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off + _task->disableBluetooth(); } else { - _task->enableSerial(); + _task->enableBluetooth(); } return true; } @@ -677,7 +677,7 @@ void UITask::loop() { _cached_batt_mv, isBuzzerQuiet(), getGPSState(), - isSerialEnabled()); + isBluetoothEnabled()); bool status_dirty = _statusBar.needsRedraw(); bool content_dirty = (millis() >= _next_refresh && curr); From 4050b7d5261a8ae52278b949ea2a43a68a052cfb Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:14:59 +1000 Subject: [PATCH 166/191] nRF52840 "start ota" Failure Recovery If Bluefruit.begin fails, BLE OTA mode won't actually start and the board might require a reboot to reattempt. Fixes: - Bluefruit.begin returns false in NRF52Board.startOTAUpdate if OTA mode fails to start. User is notified of the fault through existing error message in CommonCLI and can reattempt "start ota" command. --- src/helpers/NRF52Board.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 23f7cafc2..b6c8fec56 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -396,7 +396,8 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); - Bluefruit.begin(1, 0); + if (!Bluefruit.begin(1, 0)) return false; + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 Bluefruit.setTxPower(4); // Set the BLE device name From 3a9f19c14b2537e77f5297ff5e3ab5578d84c266 Mon Sep 17 00:00:00 2001 From: Che177 Date: Mon, 27 Jul 2026 19:32:47 -0700 Subject: [PATCH 167/191] room_server: add room.post system posts --- examples/simple_room_server/MyMesh.cpp | 42 +++++++++++++++++++++++--- examples/simple_room_server/MyMesh.h | 2 ++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 7bf941a94..e7cd2d75a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -39,18 +39,43 @@ struct ServerStats { }; void MyMesh::addPost(ClientInfo *client, const char *postData) { - // TODO: suggested postData format: /<descrption> - posts[next_post_idx].author = client->id; // add to cyclic queue - StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN); + storePost(client->id, postData); +} - posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); +void MyMesh::addSystemPost(const char *postData) { + if (!postData || postData[0] == 0) return; + +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.print("room.post: addSystemPost: "); Serial.println(postData); +#endif + + storePost(self_id, postData); +} + +void MyMesh::storePost(const mesh::Identity &author, const char *postData) { + int idx = next_post_idx; + // TODO: suggested postData format: <title>/<descrption> + posts[idx].author = author; // add to cyclic queue + StrHelper::strncpy(posts[idx].text, postData, MAX_POST_TEXT_LEN); + + posts[idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.printf("room.post: storePost idx=%d text=%s\n", idx, posts[idx].text); + Serial.printf("room.post: timestamp=%u\n", posts[idx].post_timestamp); +#endif next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); _num_posted++; // stats +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.printf("room.post: next_post_idx=%d num_posted=%d push scheduled\n", next_post_idx, _num_posted); +#endif } void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.print("room.post: pushPostToClient text="); Serial.println(post.text); +#endif int len = 0; memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client @@ -948,6 +973,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strncmp(command, "room.post", 9) == 0) { + char* msg = command + 9; + while (*msg == ' ') msg++; + if (*msg == 0) { + snprintf(reply, MAX_POST_TEXT_LEN, "ERR empty message"); + } else { + addSystemPost(msg); + snprintf(reply, MAX_POST_TEXT_LEN, "OK"); + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index caef69209..5f78bee81 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int matching_peer_indexes[MAX_CLIENTS]; void addPost(ClientInfo* client, const char* postData); + void storePost(const mesh::Identity& author, const char* postData); void pushPostToClient(ClientInfo* client, PostInfo& post); uint8_t getUnsyncedCount(ClientInfo* client); bool processAck(const uint8_t *data); @@ -176,6 +177,7 @@ public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); + void addSystemPost(const char* postData); const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } From 5a04e060a59a986758f7d0b4619280598b7dc6d2 Mon Sep 17 00:00:00 2001 From: Che177 <github@kitagor.com> Date: Tue, 28 Jul 2026 21:57:42 -0700 Subject: [PATCH 168/191] room_server: use standard mesh debug logging --- examples/simple_room_server/MyMesh.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index e7cd2d75a..0aff39cc1 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -45,9 +45,7 @@ void MyMesh::addPost(ClientInfo *client, const char *postData) { void MyMesh::addSystemPost(const char *postData) { if (!postData || postData[0] == 0) return; -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.print("room.post: addSystemPost: "); Serial.println(postData); -#endif + MESH_DEBUG_PRINTLN("room.post: addSystemPost: %s", postData); storePost(self_id, postData); } @@ -59,23 +57,17 @@ void MyMesh::storePost(const mesh::Identity &author, const char *postData) { StrHelper::strncpy(posts[idx].text, postData, MAX_POST_TEXT_LEN); posts[idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.printf("room.post: storePost idx=%d text=%s\n", idx, posts[idx].text); - Serial.printf("room.post: timestamp=%u\n", posts[idx].post_timestamp); -#endif + MESH_DEBUG_PRINTLN("room.post: storePost idx=%d text=%s", idx, posts[idx].text); + MESH_DEBUG_PRINTLN("room.post: timestamp=%u", posts[idx].post_timestamp); next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); _num_posted++; // stats -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.printf("room.post: next_post_idx=%d num_posted=%d push scheduled\n", next_post_idx, _num_posted); -#endif + MESH_DEBUG_PRINTLN("room.post: next_post_idx=%d num_posted=%d push scheduled", next_post_idx, _num_posted); } void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.print("room.post: pushPostToClient text="); Serial.println(post.text); -#endif + MESH_DEBUG_PRINTLN("room.post: pushPostToClient text=%s", post.text); int len = 0; memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client From d7f378b489e9e298effb993af0700a3a21f974ad Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:27:15 +1000 Subject: [PATCH 169/191] Clean up debugs left on in platform.ini files --- variants/meshadventurer/platformio.ini | 8 ++++---- variants/minewsemi_me25ls01/platformio.ini | 16 ++++++++-------- variants/muziworks_r1_neo/platformio.ini | 2 +- variants/promicro/platformio.ini | 2 +- variants/rak4631/platformio.ini | 2 +- variants/station_g2/platformio.ini | 4 ++-- variants/station_g3_esp32/platformio.ini | 2 +- variants/thinknode_m2/platformio.ini | 2 +- variants/thinknode_m3/platformio.ini | 2 +- variants/thinknode_m5/platformio.ini | 2 +- variants/thinknode_m7/platformio.ini | 2 +- variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_rp2040/platformio.ini | 4 ++-- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/variants/meshadventurer/platformio.ini b/variants/meshadventurer/platformio.ini index f85be2388..5def659ce 100644 --- a/variants/meshadventurer/platformio.ini +++ b/variants/meshadventurer/platformio.ini @@ -209,8 +209,8 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 lib_deps = ${Meshadventurer.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -289,8 +289,8 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 lib_deps = ${Meshadventurer.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index ac81fa16b..97c6c11e3 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -46,8 +46,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -67,8 +67,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -112,8 +112,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -138,8 +138,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=40 ;-D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 3dbecf1e8..13795879b 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -127,7 +127,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${R1Neo.build_src_filter} +<../examples/simple_sensor> diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e1586..01fd8932a 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -143,7 +143,7 @@ build_flags = ${Promicro.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${Promicro.build_src_filter} +<helpers/nrf52/SerialBLEInterface.cpp> +<helpers/ui/SSD1306Display.cpp> diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 31b507b4c..ea900c6c1 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -254,7 +254,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} +<helpers/ui/SSD1306Display.cpp> +<../examples/simple_sensor> diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 508ffe4b5..ffbf387a1 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -110,7 +110,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 -D SX126X_RX_BOOSTED_GAIN=1 ; https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance ; -D MESH_DEBUG=1 @@ -152,7 +152,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 -D SX126X_RX_BOOSTED_GAIN=1 -D WITH_ESPNOW_BRIDGE=1 ; -D BRIDGE_DEBUG=1 diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index b29388ef7..71bad18be 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -71,7 +71,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} +<../examples/simple_repeater> diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index aae83324e..cc9f7688f 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -30,7 +30,7 @@ build_flags = ${esp32_base.build_flags} -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_RX_BOOSTED_GAIN=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${esp32_base.build_src_filter} +<helpers/ui/SH1106Display.cpp> +<helpers/ui/MomentaryButton.cpp> diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index f80a61618..39eaed842 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -26,7 +26,7 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_TX_LED=PIN_LED_BLUE -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D ENV_INCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<helpers/*.cpp> diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 8572d1ebe..cdbcceeff 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -37,7 +37,7 @@ build_flags = ${esp32_base.build_flags} -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_RX_BOOSTED_GAIN=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D ENV_INCLUDE_GPS=1 -D PERSISTANT_GPS=1 -D ENV_SKIP_GPS_DETECT=1 diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index acd98c8c7..c4bed0172 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -119,7 +119,7 @@ build_flags = -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' -D OFFLINE_QUEUE_SIZE=256 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} +<helpers/esp32/*.cpp> +<helpers/ui/MomentaryButton.cpp> diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a08543368..56e27cbbb 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -53,7 +53,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D QSPIFLASH=1 build_src_filter = ${Xiao_nrf52.build_src_filter} +<helpers/nrf52/SerialBLEInterface.cpp> diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini index ca00e38b5..62d360054 100644 --- a/variants/xiao_rp2040/platformio.ini +++ b/variants/xiao_rp2040/platformio.ini @@ -38,8 +38,8 @@ build_flags = ${Xiao_rp2040.build_flags} -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 build_src_filter = ${Xiao_rp2040.build_src_filter} +<../examples/simple_repeater> From 20f55a4f42d11b2859734bab9db2224cc2e3a6fa Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 16:10:15 +1000 Subject: [PATCH 170/191] fix unnecessary IRQ clear --- src/helpers/radiolib/CustomSX1262.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 6a7c21440..b4ee6c97a 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -117,6 +117,12 @@ class CustomSX1262 : public SX1262 { _headerSeen = false; return false; } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + if (header) { if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; if (now - _activityAt > _maxPayloadMillis) { From a7426de0bf938c089531e5e2b2177d989fbc07e7 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:18:15 +1000 Subject: [PATCH 171/191] LR1110: fix unnecessary IRQ clear --- src/helpers/radiolib/CustomLR1110.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 20aea9ff3..c481cf5e0 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -52,6 +52,11 @@ class CustomLR1110 : public LR1110 { _headerSeen = false; return false; } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } if (header) { if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; if (now - _activityAt > _maxPayloadMillis) { From a5c323cc1939dd3b2b8789c19f0c258ac2adb257 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:19:40 +1000 Subject: [PATCH 172/191] STM32WLx: add IRQ timeout logic --- src/helpers/radiolib/CustomSTM32WLx.h | 58 +++++++++++++++++++- src/helpers/radiolib/CustomSTM32WLxWrapper.h | 3 + 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomSTM32WLx.h b/src/helpers/radiolib/CustomSTM32WLx.h index 01190d065..e312a5ee4 100644 --- a/src/helpers/radiolib/CustomSTM32WLx.h +++ b/src/helpers/radiolib/CustomSTM32WLx.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomSTM32WLx : public STM32WLx { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } @@ -12,8 +17,55 @@ class CustomSTM32WLx : public STM32WLx { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820d..a792a8775 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -15,6 +15,9 @@ public: ((CustomSTM32WLx *)_radio)->setBandwidth(bw); ((CustomSTM32WLx *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSTM32WLx *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSTM32WLx *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From a3732e1c65433a0e2bdbd825c58c908a36091699 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:29:37 +1000 Subject: [PATCH 173/191] SX1268: add IRQ timeout logic --- src/helpers/radiolib/CustomSX1268.h | 58 ++++++++++++++++++++-- src/helpers/radiolib/CustomSX1268Wrapper.h | 3 ++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index 1e71a3fa6..f915332dc 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomSX1268 : public SX1268 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSX1268(Module *mod) : SX1268(mod) { } @@ -81,10 +86,57 @@ class CustomSX1268 : public SX1268 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + bool getRxBoostedGainMode() { uint8_t rxGain = 0; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index bce56b996..104ba08b2 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -18,6 +18,9 @@ public: ((CustomSX1268 *)_radio)->setBandwidth(bw); ((CustomSX1268 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSX1268 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSX1268 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From 79eb7f5d8dd223bf19303099dfff3adbd54205b3 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:33:24 +1000 Subject: [PATCH 174/191] LLCC68: add IRQ timeout logic --- src/helpers/radiolib/CustomLLCC68.h | 57 ++++++++++++++++++++-- src/helpers/radiolib/CustomLLCC68Wrapper.h | 4 ++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index bde44b6ca..1dcd916fa 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomLLCC68 : public LLCC68 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomLLCC68(Module *mod) : LLCC68(mod) { } @@ -81,9 +86,55 @@ class CustomLLCC68 : public LLCC68 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 851fd644b..ae0fe0a25 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -14,6 +14,10 @@ public: ((CustomLLCC68 *)_radio)->setBandwidth(bw); ((CustomLLCC68 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLLCC68 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLLCC68 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); + } bool isReceivingPacket() override { From 78723d25654f7955adff59b434ccbe3bffc37967 Mon Sep 17 00:00:00 2001 From: Mike Damon <mike.damon@lioinsurance.com> Date: Thu, 30 Jul 2026 10:58:40 -0400 Subject: [PATCH 175/191] LR1110: fix startReceive() passing an IRQ bit as the RX timeout CustomLR1110::startReceive() passed RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1<<4 = 16) as RadioLib's first argument, which is the RX *timeout*, not an IRQ mask. At the LR11x0's 30.52us tick that armed the receiver for ~488us, so it dropped out of RX before any packet could arrive and the node received nothing at all -- while transmitting normally. Symptoms on a SenseCAP T1000-E: tx_air_secs rising, rx_air_secs stuck at 0, recv_errors 0, and the noise floor pinned at the -120 clamp because getCurrentRSSI() never sampled a live receiver. Pass RADIOLIB_LR11X0_RX_TIMEOUT_INF (continuous RX), which is what LR11x0::startReceive() itself uses, keeping the PREAMBLE_DETECTED flag in the reported IRQ flags as intended. Introduced in ea5d7c8b ("LR1110: add PREAMBLE_DETECTED to reported irq flags"). Verified on two T1000-E units: with only the repeater fixed it began receiving (last_rssi -29, SNR 17.0) while the unfixed companion stayed deaf; fixing both brought up the link in each direction. --- src/helpers/radiolib/CustomLR1110.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index c481cf5e0..56a9aa6ef 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -36,8 +36,15 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } int16_t startReceive() override { - // include the PREAMBLE_DETECTED irq bit in reported flags - return LR1110::startReceive(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + // include the PREAMBLE_DETECTED irq bit in reported flags. + // + // NOTE: the first argument is the RX *timeout*, not an IRQ mask. Passing + // RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1<<4 = 16) here armed the receiver + // with a 16-tick timeout -- at the LR11x0's 30.52us tick that is a ~488us + // receive window, so the radio dropped out of RX before any packet could + // arrive and the node never received anything. It must stay RX_TIMEOUT_INF + // (continuous RX), which is what LR11x0::startReceive() itself passes. + return LR1110::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } bool isReceiving() { From ae1b610a9405793142b3be3c76c348da7cb44797 Mon Sep 17 00:00:00 2001 From: me <jiroiset@gmail.com> Date: Sat, 1 Aug 2026 22:20:20 -0700 Subject: [PATCH 176/191] fix: M5Stack Unit C6L merged.bin fails to boot (flash_mode qio->dio) Board manifest for esp32-c6-devkitm-1 defaults build.flash_mode to qio, which this module's flash chip does not support -- causes a boot crash-loop (repeated USB-Serial-JTAG reconnects) on real hardware. Override with board_build.flash_mode = dio in the common M5Stack_Unit_C6L section so it applies to all envs (ble/usb/repeater/room_server). --- variants/m5stack_unit_c6l/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index 190add043..ea4d852d6 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -2,6 +2,7 @@ extends = esp32c6_base board = esp32-c6-devkitm-1 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit +board_build.flash_mode = dio ; board manifest defaults to qio, which causes a boot crash-loop on this module build_flags = ${esp32c6_base.build_flags} ${sensor_base.build_flags} From 5d940a1dc9347c7451e1e07c059b58f6cc3cad9c Mon Sep 17 00:00:00 2001 From: Huw Duddy <37787853+oltaco@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:57:34 +1000 Subject: [PATCH 177/191] Clean up comments in startReceive method Removed unnecessary comments regarding RX timeout and IRQ mask. --- src/helpers/radiolib/CustomLR1110.h | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 56a9aa6ef..75674a1df 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -37,13 +37,6 @@ class CustomLR1110 : public LR1110 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags. - // - // NOTE: the first argument is the RX *timeout*, not an IRQ mask. Passing - // RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1<<4 = 16) here armed the receiver - // with a 16-tick timeout -- at the LR11x0's 30.52us tick that is a ~488us - // receive window, so the radio dropped out of RX before any packet could - // arrive and the node never received anything. It must stay RX_TIMEOUT_INF - // (continuous RX), which is what LR11x0::startReceive() itself passes. return LR1110::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } @@ -99,4 +92,4 @@ class CustomLR1110 : public LR1110 { } uint8_t getSpreadingFactor() const { return spreadingFactor; } -}; \ No newline at end of file +}; From 0fd11ed223b5095aed83482c170637b040af2c29 Mon Sep 17 00:00:00 2001 From: Scott Powell <ripple_biz@protonmail.com> Date: Tue, 4 Aug 2026 01:00:31 +1000 Subject: [PATCH 178/191] * bounds check added for anon_req reply_path_len --- examples/simple_repeater/MyMesh.cpp | 28 ++++++++++++---------------- examples/simple_repeater/MyMesh.h | 3 +-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 09f74cbea..c7e651a18 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -147,11 +147,10 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data & 63; - reply_path_hash_size = (*data >> 6) + 1; - data++; + reply_path_len = *data++; + if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding - memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + mesh::Packet::writePath(reply_path, data, reply_path_len); // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag @@ -166,11 +165,10 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data & 63; - reply_path_hash_size = (*data >> 6) + 1; - data++; + reply_path_len = *data++; + if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding - memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + mesh::Packet::writePath(reply_path, data, reply_path_len); // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag @@ -186,11 +184,10 @@ uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data & 63; - reply_path_hash_size = (*data >> 6) + 1; - data++; + reply_path_len = *data++; + if (!mesh::Packet::isValidPathLen(reply_path_len)) return 0; // reject - bad encoding - memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + mesh::Packet::writePath(reply_path, data, reply_path_len); // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag @@ -574,7 +571,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m data[len] = 0; // ensure null terminator uint8_t reply_len; - reply_path_len = -1; + reply_path_len = 0xFF; if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood()); } else if (data[4] == ANON_REQ_TYPE_REGIONS && packet->isRouteDirect()) { @@ -594,13 +591,12 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); - } else if (reply_path_len < 0) { + } else if (reply_path_len == 0xFF) { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63); - if (reply) sendDirect(reply, reply_path, path_len, SERVER_RESPONSE_DELAY); + if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); } } } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index aa7d30b06..19022b77b 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -92,8 +92,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; uint8_t reply_path[MAX_PATH_SIZE]; - int8_t reply_path_len; - uint8_t reply_path_hash_size; + uint8_t reply_path_len; TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* load_stack[8]; From 7cc16366cb522b7a39d6c3ae5393e0aa369b75fb Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 5 Aug 2026 14:23:29 +1000 Subject: [PATCH 179/191] add Meshnology W12 support and LR2021 wrapper --- boards/meshnology_w12.json | 43 ++++ src/helpers/radiolib/CustomLR2021.h | 76 ++++++ src/helpers/radiolib/CustomLR2021Wrapper.h | 57 +++++ src/helpers/radiolib/RadioLibWrappers.cpp | 4 + .../meshnology_w12/MeshnologyW12Board.cpp | 83 +++++++ variants/meshnology_w12/MeshnologyW12Board.h | 37 +++ variants/meshnology_w12/pins_arduino.h | 69 ++++++ variants/meshnology_w12/platformio.ini | 216 ++++++++++++++++++ variants/meshnology_w12/target.cpp | 73 ++++++ variants/meshnology_w12/target.h | 28 +++ 10 files changed, 686 insertions(+) create mode 100644 boards/meshnology_w12.json create mode 100644 src/helpers/radiolib/CustomLR2021.h create mode 100644 src/helpers/radiolib/CustomLR2021Wrapper.h create mode 100644 variants/meshnology_w12/MeshnologyW12Board.cpp create mode 100644 variants/meshnology_w12/MeshnologyW12Board.h create mode 100644 variants/meshnology_w12/pins_arduino.h create mode 100644 variants/meshnology_w12/platformio.ini create mode 100644 variants/meshnology_w12/target.cpp create mode 100644 variants/meshnology_w12/target.h diff --git a/boards/meshnology_w12.json b/boards/meshnology_w12.json new file mode 100644 index 000000000..2e74af9b3 --- /dev/null +++ b/boards/meshnology_w12.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x0002"], ["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "meshnology_w12" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v5 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h new file mode 100644 index 000000000..17944ef00 --- /dev/null +++ b/src/helpers/radiolib/CustomLR2021.h @@ -0,0 +1,76 @@ +#pragma once + +#include <RadioLib.h> +#include "MeshCore.h" + +class CustomLR2021 : public LR2021 { + bool _rx_boosted = false; + + public: + CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; } + + bool std_init(SPIClass* spi = NULL) + { + + #ifdef LR2021_TCXO_VOLTAGE + float tcxo = LR2021_TCXO_VOLTAGE; + #else + float tcxo = 1.6f; + #endif + + #ifdef LORA_CR + uint8_t cr = LORA_CR; + #else + uint8_t cr = 5; + #endif + + #if defined(P_LORA_SCLK) + #ifdef NRF52_PLATFORM + if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); } + #elif defined(RP2040_PLATFORM) + if (spi) { + spi->setMISO(P_LORA_MISO); + //spi->setCS(P_LORA_NSS); // Setting CS results in freeze + spi->setSCK(P_LORA_SCLK); + spi->setMOSI(P_LORA_MOSI); + spi->begin(); + } + #else + 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_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + // 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) { + tcxo = 0.0f; + status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_LR2021_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 + } + + setCRC(2); + explicitHeader(); + + + #ifdef LR2021_RX_BOOSTED_GAIN + setRxBoostedGainMode(LR2021_RX_BOOSTED_GAIN); + #endif + + return true; // success + } + + float getFreqMHz() const { return freqMHz; } + + bool getRxBoostedGainMode() const { return _rx_boosted; } + + bool isReceiving() { + uint32_t irq = getIrqStatus(); + bool detected = ((irq & RADIOLIB_LR2021_IRQ_SYNCWORD_VALID) || (irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED)); + return detected; + } + + uint8_t getSpreadingFactor() const { return spreadingFactor; } +}; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h new file mode 100644 index 000000000..0f1b1f9e5 --- /dev/null +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -0,0 +1,57 @@ +#pragma once + +#include "CustomLR2021.h" +#include "RadioLibWrappers.h" + +#ifndef USE_LR2021 +#define USE_LR2021 +#endif + +#ifndef LR2021_RX_BOOST_LEVEL +#define LR2021_RX_BOOST_LEVEL 7 +#endif + +class CustomLR2021Wrapper : public RadioLibWrapper { +public: + CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + + void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + ((CustomLR2021 *)_radio)->setFrequency(freq); + ((CustomLR2021 *)_radio)->setSpreadingFactor(sf); + ((CustomLR2021 *)_radio)->setBandwidth(bw); + ((CustomLR2021 *)_radio)->setCodingRate(cr); + updatePreamble(sf); + } + + bool isReceivingPacket() override { + return ((CustomLR2021 *)_radio)->isReceiving(); + } + + float getCurrentRSSI() override { + float rssi = -110; + ((CustomLR2021 *)_radio)->getRssiInst(&rssi); + return rssi; + } + + void onSendFinished() override { + RadioLibWrapper::onSendFinished(); + _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts + } + + float getLastRSSI() const override { return ((CustomLR2021 *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((CustomLR2021 *)_radio)->getSNR(); } + + uint8_t getSpreadingFactor() const override { return ((CustomLR2021 *)_radio)->getSpreadingFactor(); } + + bool setRxBoostedGainMode(bool en) override { + ((CustomLR2021 *)_radio)->standby(); // radio must be in standby to accept the setRxBoostedGainMode command, otherwise it returns -707 error. + int16_t status = ((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL: 0); + RadioLibWrapper::idle(); // trigger startReceive() + return status == RADIOLIB_ERR_NONE; + } + + bool getRxBoostedGainMode() const override { + return ((CustomLR2021 *)_radio)->getRxBoostedGainMode(); + } + +}; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 7146e47b6..c2bd6b264 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -133,7 +133,11 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { n_recv++; } } + #if defined(USE_LR2021) + state = STATE_RX; // LR2021 stays in Rx after readData, if we issue another startReceive while still in Rx we get -706 errors. + #else state = STATE_IDLE; // need another startReceive() + #endif } if (state != STATE_RX) { diff --git a/variants/meshnology_w12/MeshnologyW12Board.cpp b/variants/meshnology_w12/MeshnologyW12Board.cpp new file mode 100644 index 000000000..25c0d4f1a --- /dev/null +++ b/variants/meshnology_w12/MeshnologyW12Board.cpp @@ -0,0 +1,83 @@ +#include "MeshnologyW12Board.h" + +void MeshnologyW12Board::begin() { + ESP32Board::begin(); + + pinMode(PIN_ADC_CTRL, OUTPUT); + digitalWrite(PIN_ADC_CTRL, LOW); // Disable battery sense until required + + pinMode(P_LORA_LF_PA_POWER, OUTPUT); + digitalWrite(P_LORA_LF_PA_POWER, HIGH); // PA_EN_M — power the GC1109 FEM + + pinMode(P_LORA_HF_PA_POWER, OUTPUT); + digitalWrite(P_LORA_HF_PA_POWER, LOW); // PA_EN_G — disable the 2.4GHz FEM + + delay(100); // give the GC1109 some time to start + + periph_power.begin(); + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } + } + + void MeshnologyW12Board::onBeforeTransmit(void) { + neopixelWrite(NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White) + } + + void MeshnologyW12Board::onAfterTransmit(void) { + neopixelWrite(NEOPIXEL_LED, 0, 0, 0); // turn TX neopixel off + } + + void MeshnologyW12Board::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 MeshnologyW12Board::powerOff() { + enterDeepSleep(0); + } + + uint16_t MeshnologyW12Board::getBattMilliVolts() { + analogReadResolution(12); + analogSetAttenuation(ADC_11db); + digitalWrite(PIN_ADC_CTRL, HIGH); + delay(10); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / 8; + + digitalWrite(PIN_ADC_CTRL, LOW); + + return (adc_mult * (3.3 / 4096.0) * raw) * 1000; + } + + const char* MeshnologyW12Board::getManufacturerName() const { + return "Meshnology W12"; + } \ No newline at end of file diff --git a/variants/meshnology_w12/MeshnologyW12Board.h b/variants/meshnology_w12/MeshnologyW12Board.h new file mode 100644 index 000000000..85bec9904 --- /dev/null +++ b/variants/meshnology_w12/MeshnologyW12Board.h @@ -0,0 +1,37 @@ +#pragma once + +#include <Arduino.h> +#include <helpers/RefCountedDigitalPin.h> +#include <helpers/ESP32Board.h> +#include <driver/rtc_io.h> + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER 5.42 +#endif + +class MeshnologyW12Board : public ESP32Board { + +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + MeshnologyW12Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + 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; + bool setAdcMultiplier(float multiplier) override { + if (multiplier == 0.0f) { + adc_mult = ADC_MULTIPLIER; + } else { + adc_mult = multiplier; + } + return true; + } + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/meshnology_w12/pins_arduino.h b/variants/meshnology_w12/pins_arduino.h new file mode 100644 index 000000000..5cfb36970 --- /dev/null +++ b/variants/meshnology_w12/pins_arduino.h @@ -0,0 +1,69 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include <stdint.h> + +static const uint8_t LED_BUILTIN = -1; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 3; +static const uint8_t SCL = 4; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +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; + +static const uint8_t Vext = 45; +static const uint8_t LED = -1; +static const uint8_t RST_OLED = 21; +static const uint8_t SCL_OLED = 18; +static const uint8_t SDA_OLED = 17; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + + + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini new file mode 100644 index 000000000..1168f10a0 --- /dev/null +++ b/variants/meshnology_w12/platformio.ini @@ -0,0 +1,216 @@ +[meshnology_w12] +extends = esp32_base +board = meshnology_w12 +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/meshnology_w12 + -D MESHNOLOGY_W12 + -D ESP32_CPU_FREQ=80 + -D RADIO_CLASS=CustomLR2021 + -D WRAPPER_CLASS=CustomLR2021Wrapper + -D USE_LR2021 + -D NEOPIXEL_LED=46 + -D NEOPIXEL_BRIGHTNESS=64 + -D P_LORA_DIO_1=14 + -D LR2021_IRQ_DIO=8 ; GPIO14 is connected to LR2021 DIO8 + -D P_LORA_NSS=8 + -D P_LORA_SCLK=9 + -D P_LORA_MOSI=10 + -D P_LORA_MISO=11 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D LR2021_TCXO_VOLTAGE=0.0f ; this board has no tcxo + -D RF_SWITCH_TABLE ; used below in commented code for the RF switch table. + -D P_LORA_LF_PA_POWER=4 ; PA_EN_M - power enable for the GC1109 868/915 PA + -D P_LORA_HF_PA_POWER=3 ; PA_EN_G - power enable for RFX2402E 2.4G PA + -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=45 + -D PIN_VEXT_EN_ACTIVE=HIGH + -D LORA_TX_POWER=4 + -D MAX_LORA_TX_POWER=4 ; GC1109 datasheet says max input at TX port is +5dbm, confirmed full saturation seems to occur at around 3-4dbm + -D PIN_GPS_RX=38 + -D PIN_GPS_TX=39 + -D PIN_GPS_RESET=42 + -D PIN_GPS_RESET_ACTIVE=LOW + -D PIN_GPS_EN=48 + -D PIN_GPS_EN_ACTIVE=LOW + ; -D ENV_INCLUDE_GPS=1 + -D PIN_VBAT_READ=1 + -D PIN_ADC_CTRL=2 + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D PIN_RESET=47 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/meshnology_w12> + +<helpers/sensors> +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[env:meshnology_w12_repeater] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Meshnology W12 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 = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_repeater> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:meshnology_w12_repeater_bridge_espnow] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.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 + ; -D BRIDGE_DEBUG=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/bridges/ESPNowBridge.cpp> + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_repeater> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + +[env:meshnology_w12_room_server] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Meshnology W12 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_room_server> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + +[env:meshnology_w12_terminal_chat] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${meshnology_w12.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:meshnology_w12_companion_radio_usb] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<helpers/ui/MomentaryButton.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${meshnology_w12.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:meshnology_w12_companion_radio_ble] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 ; dynamic, random PIN + ; -D AUTO_SHUTDOWN_MILLIVOLTS=3400 ; disabled by default, otherwise reading bounce causes shutdown when there's no battery connected. + ; -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<helpers/ui/MomentaryButton.cpp> + +<helpers/esp32/*.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${meshnology_w12.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:meshnology_w12_companion_radio_wifi] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SSD1306Display + ; -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<helpers/ui/MomentaryButton.cpp> + +<helpers/esp32/*.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${meshnology_w12.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:meshnology_w12_sensor] +extends = meshnology_w12 +build_flags = + ${meshnology_w12.build_flags} + -D ADVERT_NAME='"Meshnology W12 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=3 + -D ENV_PIN_SCL=4 + -D DISPLAY_CLASS=SSD1306Display + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${meshnology_w12.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_sensor> +lib_deps = + ${meshnology_w12.lib_deps} + ${esp32_ota.lib_deps} + +[env:meshnology_w12_kiss_modem] +extends = meshnology_w12 +build_src_filter = ${meshnology_w12.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/meshnology_w12/target.cpp b/variants/meshnology_w12/target.cpp new file mode 100644 index 000000000..bf04ba364 --- /dev/null +++ b/variants/meshnology_w12/target.cpp @@ -0,0 +1,73 @@ +#include <Arduino.h> +#include "target.h" + +MeshnologyW12Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include <helpers/sensors/MicroNMEALocationProvider.h> + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display(NULL); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +const uint32_t rfswitch_dios[] = { + RADIOLIB_LR2021_DIO5, // RFX2402E 2.4G_TX_EN + RADIOLIB_LR2021_DIO6, // RFX2402E 2.4G_RX_EN + RADIOLIB_LR2021_DIO9, // GC1109 CTX (Transmit mode) + RADIOLIB_LR2021_DIO10, // GC1109 CPS (Bypass mode) + RADIOLIB_LR2021_DIO11, // GC1109 CSD (Shutdown) +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // DIO5 DIO6 DIO9 DIO10 DIO11 + { LR2021::MODE_STBY, { LOW, LOW, LOW, LOW, LOW } }, // everything off + { LR2021::MODE_RX, { LOW, LOW, LOW, LOW, HIGH } }, // rx_lf: GC1109 LNA + { LR2021::MODE_TX, { LOW, LOW, HIGH, HIGH, HIGH } }, // tx_lf: GC1109 full PA + { LR2021::MODE_RX_HF, { LOW, HIGH, LOW, LOW, LOW } }, // rx_hf: 2G4 LNA, GC1109 off + { LR2021::MODE_TX_HF, { HIGH, LOW, LOW, LOW, LOW } }, // tx_hf: 2G4 PA, GC1109 off + END_OF_MODE_TABLE, +}; + + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + int err = radio.std_init(&spi); + if (err != 1) return err; +#else + int err = radio.std_init(); + if (err != 1) return err; +#endif + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif + + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/meshnology_w12/target.h b/variants/meshnology_w12/target.h new file mode 100644 index 000000000..1bf8876b2 --- /dev/null +++ b/variants/meshnology_w12/target.h @@ -0,0 +1,28 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include <RadioLib.h> +#include <helpers/radiolib/RadioLibWrappers.h> +#include <MeshnologyW12Board.h> +#include <helpers/radiolib/CustomLR2021Wrapper.h> +#include <helpers/AutoDiscoverRTCClock.h> +#include <helpers/SensorManager.h> +#include <helpers/sensors/EnvironmentSensorManager.h> +#ifdef DISPLAY_CLASS + #include <helpers/ui/SSD1306Display.h> + #include <helpers/ui/MomentaryButton.h> +#endif + +extern MeshnologyW12Board 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(); + From 696a82d7c235c09dac4b071d97df3b9e01ff8f30 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 1 Jul 2026 18:17:48 +1000 Subject: [PATCH 180/191] LR2021 multi-SF support (side detectors) --- examples/simple_repeater/MyMesh.cpp | 6 +++ examples/simple_repeater/MyMesh.h | 4 ++ src/helpers/CommonCLI.cpp | 30 ++++++++++++++ src/helpers/CommonCLI.h | 7 ++++ src/helpers/radiolib/CustomLR2021Wrapper.h | 47 +++++++++++++++++++++- src/helpers/radiolib/RadioLibWrappers.h | 2 + 6 files changed, 95 insertions(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 09f74cbea..cc0cc1f1a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1064,6 +1064,12 @@ bool MyMesh::setRxBoostedGain(bool enable) { return radio_driver.setRxBoostedGainMode(enable); } +#if defined(USE_LR2021) +bool MyMesh::configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { + return radio_driver.configSideDetectors(sideDetSFs, num); +} +#endif + void MyMesh::formatNeighborsReply(char *reply) { char *dp = reply; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index aa7d30b06..6a260e339 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -255,4 +255,8 @@ public: bool setRxBoostedGain(bool enable) override; + #if defined(USE_LR2021) + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) override; + #endif + }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9d0571d98..bb54197f5 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -753,6 +753,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported"); }; + #if defined(USE_LR2021) + } else if (memcmp(config, "extra.sf ", 9) == 0) { + strcpy(tmp, &config[9]); + const char *parts[4]; + uint8_t sideDetSFs[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + if (num > 3) { + sprintf(reply, "Invalid extra SF config"); + } else { + for (int i = 0; i < num; i++) { + sideDetSFs[i] = atoi(parts[i]); + } + sideDetSFs[num] = 0; + if (_callbacks->configSideDetectors(sideDetSFs, num)) { + for (int i = 0; i <= num; i++) _prefs->extra_sf[i] = sideDetSFs[i]; + savePrefs(); + sprintf(reply, "OK - extra SFs set"); + } else { + sprintf(reply, "Invalid extra SF config"); + } + } + #endif } else { strcpy(reply, "unknown config: "); StrHelper::strncpy(&reply[16], config, 160-17); @@ -922,6 +944,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "ERROR: Power management not supported"); #endif + } else if (memcmp(config, "extra.sf", 8) == 0) { + char* tmp = reply; + for (int i = 0; i < 3 && _prefs->extra_sf[i] != 0; i++) { + tmp += sprintf(tmp, "%s%d", (i == 0) ? "" : ",", _prefs->extra_sf[i]); + } + if (tmp == reply) { + sprintf(reply, "No extra SF configured"); + } } else { sprintf(reply, "??: %s", config); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 69fe03150..fafabcb17 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -68,6 +68,7 @@ public: uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) + uint8_t extra_sf[4]; private: class RadioPrefs : public ConfigSerializer { @@ -238,6 +239,12 @@ public: virtual bool setRxBoostedGain(bool enable) { return false; // CommonCLI reports unsupported if not overridden by wrapper }; + + #if defined(USE_LR2021) + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { + return false; // Override in wrapper + } + #endif }; class CommonCLI { diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 0f1b1f9e5..6d0204ac1 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -21,6 +21,46 @@ public: ((CustomLR2021 *)_radio)->setBandwidth(bw); ((CustomLR2021 *)_radio)->setCodingRate(cr); updatePreamble(sf); + applySideDetectorConfig(); + } + + bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num) override { + LR2021LoRaSideDetector_t tmp[3]; + uint8_t sf = getSpreadingFactor(); + + if (sf >= 10 && num > 1) { return false; } // only 1 side detector allowed when primary SF >= 10 + for (int i = 0; i < num; i++) { + if (sideDetSFs[i] > 12 || sideDetSFs[i] < 5) { return false; } // must be valid SF + if (sideDetSFs[i] <= sf) { return false; } // must be < primary SF + if (sideDetSFs[i] > sf + 4) { return false; } // span must not be > 4 + + tmp[i].sf = sideDetSFs[i]; + if (sideDetSFs[i] == 10) { // TODO: set ldro=true when tSym >=16 + tmp[i].ldro = true; + } else { + tmp[i].ldro = false; + } + tmp[i].invertIQ = false; + tmp[i].syncWord = 0x12; + } + int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(tmp, num); + RadioLibWrapper::idle(); // trigger startReceive() + MESH_DEBUG_PRINTLN("setSideDetector() returned %d", status); + + if (status == RADIOLIB_ERR_NONE) { + for (int i = 0; i < num; i++) { _sideDet[i] = tmp[i]; } + _numSideDet = num; + } else { + return false; + } + + return true; + } + + int16_t applySideDetectorConfig() { + int16_t status = ((CustomLR2021 *)_radio)->setSideDetector(_sideDet, _numSideDet); + RadioLibWrapper::idle(); // trigger startReceive() + return status; } bool isReceivingPacket() override { @@ -44,7 +84,7 @@ public: uint8_t getSpreadingFactor() const override { return ((CustomLR2021 *)_radio)->getSpreadingFactor(); } bool setRxBoostedGainMode(bool en) override { - ((CustomLR2021 *)_radio)->standby(); // radio must be in standby to accept the setRxBoostedGainMode command, otherwise it returns -707 error. + ((CustomLR2021 *)_radio)->standby(); // LR2021 must be in standby to accept setRxBoostedGainMode int16_t status = ((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL: 0); RadioLibWrapper::idle(); // trigger startReceive() return status == RADIOLIB_ERR_NONE; @@ -54,4 +94,9 @@ public: return ((CustomLR2021 *)_radio)->getRxBoostedGainMode(); } + protected: + LR2021LoRaSideDetector_t _sideDet[3]; + size_t _numSideDet = 0; + + }; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index e886c81b2..4ca958e47 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -77,6 +77,8 @@ public: virtual bool setRxBoostedGainMode(bool) { return false; } virtual bool getRxBoostedGainMode() const { return false; } + + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { return false; } }; /** From 36e77671a24a8e3c228c2e0554f6fd11ac2579de Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Thu, 2 Jul 2026 17:18:03 +1000 Subject: [PATCH 181/191] LR2021: fix for hardware cad and side detectors --- 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 c2bd6b264..dc851e5cf 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -105,6 +105,9 @@ void RadioLibWrapper::loop() { } void RadioLibWrapper::startRecv() { + #if defined(USE_LR2021) + _radio->standby(); // without this LR2021 can throw -706 when calling startReceive after hardware CAD when side detectors are enabled + #endif int err = _radio->startReceive(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; @@ -134,7 +137,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { } } #if defined(USE_LR2021) - state = STATE_RX; // LR2021 stays in Rx after readData, if we issue another startReceive while still in Rx we get -706 errors. + state = STATE_RX; // LR2021 stays in Rx after readData, calling startReceive while still in Rx throws -706 errors #else state = STATE_IDLE; // need another startReceive() #endif From 9d4f93806d83abd593bbaae51c2944cac12c422b Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Sun, 12 Jul 2026 18:03:25 +1000 Subject: [PATCH 182/191] LR2021: auto-LDRO for side detectors --- examples/simple_repeater/MyMesh.cpp | 4 ++-- examples/simple_repeater/MyMesh.h | 2 +- src/helpers/CommonCLI.cpp | 2 +- src/helpers/CommonCLI.h | 2 +- src/helpers/radiolib/CustomLR2021Wrapper.h | 11 ++++++++--- src/helpers/radiolib/RadioLibWrappers.h | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index cc0cc1f1a..6da8cca2c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1065,8 +1065,8 @@ bool MyMesh::setRxBoostedGain(bool enable) { } #if defined(USE_LR2021) -bool MyMesh::configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { - return radio_driver.configSideDetectors(sideDetSFs, num); +bool MyMesh::configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) { + return radio_driver.configSideDetectors(sideDetSFs, num, bw); } #endif diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6a260e339..ff35cfab2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -256,7 +256,7 @@ public: bool setRxBoostedGain(bool enable) override; #if defined(USE_LR2021) - virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) override; + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) override; #endif }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index bb54197f5..07181e16a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -766,7 +766,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep sideDetSFs[i] = atoi(parts[i]); } sideDetSFs[num] = 0; - if (_callbacks->configSideDetectors(sideDetSFs, num)) { + if (_callbacks->configSideDetectors(sideDetSFs, num, _prefs->bw)) { for (int i = 0; i <= num; i++) _prefs->extra_sf[i] = sideDetSFs[i]; savePrefs(); sprintf(reply, "OK - extra SFs set"); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index fafabcb17..2a9ec43bc 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -241,7 +241,7 @@ public: }; #if defined(USE_LR2021) - virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) { return false; // Override in wrapper } #endif diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 6d0204ac1..879898315 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -24,7 +24,7 @@ public: applySideDetectorConfig(); } - bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num) override { + bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num, float bw) override { LR2021LoRaSideDetector_t tmp[3]; uint8_t sf = getSpreadingFactor(); @@ -35,7 +35,8 @@ public: if (sideDetSFs[i] > sf + 4) { return false; } // span must not be > 4 tmp[i].sf = sideDetSFs[i]; - if (sideDetSFs[i] == 10) { // TODO: set ldro=true when tSym >=16 + float tSym = calcTsym(tmp[i].sf, bw); + if (tSym >= 16.0f) { tmp[i].ldro = true; } else { tmp[i].ldro = false; @@ -63,6 +64,11 @@ public: return status; } + float calcTsym(uint8_t sf, float bw) { + float tSym = (float)(uint32_t(1) << sf) / (float)bw; + return tSym; + } + bool isReceivingPacket() override { return ((CustomLR2021 *)_radio)->isReceiving(); } @@ -98,5 +104,4 @@ public: LR2021LoRaSideDetector_t _sideDet[3]; size_t _numSideDet = 0; - }; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 4ca958e47..99f5ebbd8 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -78,7 +78,7 @@ public: virtual bool setRxBoostedGainMode(bool) { return false; } virtual bool getRxBoostedGainMode() const { return false; } - virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num) { return false; } + virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) { return false; } }; /** From 3d6a891586b9a3e02d8c69287a2913adaf1bd914 Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:59:16 -0500 Subject: [PATCH 183/191] Add support for Seeed SenseCAP MeshTracker X1 (LR2021) --- boards/seeed-mesh-tracker-x1.json | 60 +++++++++ .../meshtracker_x1/MeshTrackerX1Board.cpp | 23 ++++ variants/meshtracker_x1/MeshTrackerX1Board.h | 87 +++++++++++++ variants/meshtracker_x1/platformio.ini | 117 +++++++++++++++++ variants/meshtracker_x1/target.cpp | 116 +++++++++++++++++ variants/meshtracker_x1/target.h | 44 +++++++ variants/meshtracker_x1/variant.cpp | 106 +++++++++++++++ variants/meshtracker_x1/variant.h | 123 ++++++++++++++++++ 8 files changed, 676 insertions(+) create mode 100644 boards/seeed-mesh-tracker-x1.json create mode 100644 variants/meshtracker_x1/MeshTrackerX1Board.cpp create mode 100644 variants/meshtracker_x1/MeshTrackerX1Board.h create mode 100644 variants/meshtracker_x1/platformio.ini create mode 100644 variants/meshtracker_x1/target.cpp create mode 100644 variants/meshtracker_x1/target.h create mode 100644 variants/meshtracker_x1/variant.cpp create mode 100644 variants/meshtracker_x1/variant.h diff --git a/boards/seeed-mesh-tracker-x1.json b/boards/seeed-mesh-tracker-x1.json new file mode 100644 index 000000000..347aca53f --- /dev/null +++ b/boards/seeed-mesh-tracker-x1.json @@ -0,0 +1,60 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"], + ["0x2886", "0x0057"] + ], + "usb_product": "X1-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_Mesh-Tracker-X1", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Seeed SenseCAP MeshTracker X1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/SenseCAP-MeshTracker-X1-for-Meshtastic-p-6793.html", + "vendor": "Seeed Studio" +} diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.cpp b/variants/meshtracker_x1/MeshTrackerX1Board.cpp new file mode 100644 index 000000000..56e537a23 --- /dev/null +++ b/variants/meshtracker_x1/MeshTrackerX1Board.cpp @@ -0,0 +1,23 @@ +#include <Arduino.h> +#include <Wire.h> + +#include "MeshTrackerX1Board.h" + +void MeshTrackerX1Board::begin() { + NRF52BoardDCDC::begin(); + btn_prev_state = LOW; // button is active HIGH + +#ifdef BUTTON_PIN + pinMode(BATTERY_PIN, INPUT); + pinMode(BUTTON_PIN, INPUT_PULLDOWN); + pinMode(LED_PIN, OUTPUT); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + + Wire.begin(); + + delay(10); // give lr2021 some time to power up +} diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.h b/variants/meshtracker_x1/MeshTrackerX1Board.h new file mode 100644 index 000000000..51f022e0b --- /dev/null +++ b/variants/meshtracker_x1/MeshTrackerX1Board.h @@ -0,0 +1,87 @@ +#pragma once + +#include <MeshCore.h> +#include <Arduino.h> +#include <helpers/NRF52Board.h> + +class MeshTrackerX1Board : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + +public: + MeshTrackerX1Board() : NRF52Board("X1_OTA") {} + void begin(); + + uint16_t getBattMilliVolts() override { + #ifdef BATTERY_PIN + #ifdef PIN_BAT_ADC_EN + digitalWrite(PIN_BAT_ADC_EN, HIGH); + #endif + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + float volts = (analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096; + + analogReference(AR_DEFAULT); // put back to default + analogReadResolution(10); + + return volts * 1000; + #else + return 0; + #endif + } + + const char* getManufacturerName() const override { + return "Seeed SenseCAP MeshTracker X1"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == USER_BTN_PRESSED) ? 1 : -1; + } + #endif + return 0; + } + + void powerOff() override { + #ifdef HAS_GPS + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + digitalWrite(GPS_EN, LOW); + #endif + + #ifdef PIN_DRV_EN + digitalWrite(PIN_DRV_EN, LOW); + #endif + + #ifdef PIN_BAT_ADC_EN + digitalWrite(PIN_BAT_ADC_EN, LOW); + #endif + + #ifdef PIN_3V3_EN + digitalWrite(PIN_3V3_EN, LOW); + #endif + + // set led on and wait for button release before poweroff + #ifdef LED_PIN + digitalWrite(LED_PIN, HIGH); + #endif + #ifdef BUTTON_PIN + while(digitalRead(BUTTON_PIN)); + #endif + #ifdef LED_PIN + digitalWrite(LED_PIN, LOW); + #endif + + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLDOWN, NRF_GPIO_PIN_SENSE_HIGH); + #endif + + sd_power_system_off(); + } +}; diff --git a/variants/meshtracker_x1/platformio.ini b/variants/meshtracker_x1/platformio.ini new file mode 100644 index 000000000..dbab5ca63 --- /dev/null +++ b/variants/meshtracker_x1/platformio.ini @@ -0,0 +1,117 @@ +[MeshTracker_X1] +extends = nrf52_base +board = seeed-mesh-tracker-x1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/meshtracker_x1 + -I src/helpers/ui + -D MESH_TRACKER_X1 + -D PIN_USER_BTN=6 + -D USER_BTN_PRESSED=HIGH + -D PIN_STATUS_LED=24 + -D RADIO_CLASS=CustomLR2021 + -D USE_LR2021 + -D WRAPPER_CLASS=CustomLR2021Wrapper + -D LORA_TX_POWER=22 + -D P_LORA_BUSY=7 ; P0.7 + -D P_LORA_SCLK=11 ; P0.11 + -D P_LORA_NSS=12 ; P0.12 + -D P_LORA_DIO_1=33 ; P1.1 + -D P_LORA_MISO=40 ; P1.8 + -D P_LORA_MOSI=41 ; P1.9 + -D P_LORA_RESET=42 ; P1.10 + -D LR2021_IRQ_DIO=8 ; P1.1 is connected to LR2021 DIO8 + -D LR2021_TCXO_VOLTAGE=1.6f + -D ENV_INCLUDE_GPS=1 +build_src_filter = ${nrf52_base.build_src_filter} + +<helpers/*.cpp> + +<../variants/meshtracker_x1> +debug_tool = jlink +upload_protocol = nrfutil + +[env:MeshTracker_X1_repeater] +extends = MeshTracker_X1 +build_flags = ${MeshTracker_X1.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"MeshTracker X1 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 = ${MeshTracker_X1.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${MeshTracker_X1.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:MeshTracker_X1_room_server] +extends = MeshTracker_X1 +build_flags = ${MeshTracker_X1.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"MeshTracker X1 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${MeshTracker_X1.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${MeshTracker_X1.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:MeshTracker_X1_companion_radio_usb] +extends = MeshTracker_X1 +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${MeshTracker_X1.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=25 + -D ENABLE_USB_INTERFACE +build_src_filter = ${MeshTracker_X1.build_src_filter} + +<helpers/ui/buzzer.cpp> + +<helpers/ui/NullDisplayDriver.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${MeshTracker_X1.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:MeshTracker_X1_companion_radio_ble] +extends = MeshTracker_X1 +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${MeshTracker_X1.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=25 + -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${MeshTracker_X1.build_src_filter} + +<helpers/nrf52/SerialBLEInterface.cpp> + +<helpers/ui/buzzer.cpp> + +<helpers/ui/NullDisplayDriver.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${MeshTracker_X1.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 diff --git a/variants/meshtracker_x1/target.cpp b/variants/meshtracker_x1/target.cpp new file mode 100644 index 000000000..1139b8b71 --- /dev/null +++ b/variants/meshtracker_x1/target.cpp @@ -0,0 +1,116 @@ +#include <Arduino.h> +#include "target.h" +#include <helpers/sensors/MicroNMEALocationProvider.h> + +MeshTrackerX1Board 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 rtc_clock; +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +MeshTrackerX1SensorManager sensors = MeshTrackerX1SensorManager(nmea); + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +bool radio_init() { + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + +void MeshTrackerX1SensorManager::start_gps() { + gps_active = true; + // this init sequence comes from seeed examples and deals with all gps pins + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); + delay(10); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + delay(10); + + pinMode(GPS_RESET, OUTPUT); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); +} + +void MeshTrackerX1SensorManager::sleep_gps() { + gps_active = false; + digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC alive for faster fix on wake + digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, LOW); +} + +void MeshTrackerX1SensorManager::stop_gps() { + gps_active = false; + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, LOW); +} + +bool MeshTrackerX1SensorManager::begin() { + // init GPS + Serial1.begin(GPS_BAUD_RATE); + return true; +} + +bool MeshTrackerX1SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void MeshTrackerX1SensorManager::loop() { + static long next_gps_update = 0; + + _nmea->loop(); + + if (millis() > next_gps_update) { + if (gps_active && _nmea->isValid()) { + node_lat = ((double)_nmea->getLatitude())/1000000.; + node_lon = ((double)_nmea->getLongitude())/1000000.; + node_altitude = ((double)_nmea->getAltitude()) / 1000.0; + } + next_gps_update = millis() + 1000; + } +} + +int MeshTrackerX1SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char* MeshTrackerX1SensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} +const char* MeshTrackerX1SensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} +bool MeshTrackerX1SensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + sleep_gps(); // sleep for faster fix ! + } else { + start_gps(); + } + return true; + } + return false; // not supported +} diff --git a/variants/meshtracker_x1/target.h b/variants/meshtracker_x1/target.h new file mode 100644 index 000000000..665f43281 --- /dev/null +++ b/variants/meshtracker_x1/target.h @@ -0,0 +1,44 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include <RadioLib.h> +#include <helpers/radiolib/RadioLibWrappers.h> +#include "MeshTrackerX1Board.h" +#include <helpers/radiolib/CustomLR2021Wrapper.h> +#include <helpers/ArduinoHelpers.h> +#include <helpers/SensorManager.h> +#include <helpers/sensors/LocationProvider.h> +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +class MeshTrackerX1SensorManager: public SensorManager { + bool gps_active = false; + LocationProvider * _nmea; + + void start_gps(); + void sleep_gps(); + void stop_gps(); +public: + MeshTrackerX1SensorManager(LocationProvider &nmea): _nmea(&nmea) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; + LocationProvider* getLocationProvider() { return _nmea; } +}; + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern MeshTrackerX1Board board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern MeshTrackerX1SensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/meshtracker_x1/variant.cpp b/variants/meshtracker_x1/variant.cpp new file mode 100644 index 000000000..a0e643801 --- /dev/null +++ b/variants/meshtracker_x1/variant.cpp @@ -0,0 +1,106 @@ +/* + * variant.cpp - Seeed SenseCAP MeshTracker X1 (nRF52840 + Semtech LR2021) + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02, AIN0 BATTERY_PIN + 3, // P0.03, LED_RED + 4, // P0.04, AIN2 VCC_ADC + 5, // P0.05, AIN3 EXT_PWR_DETECT + 6, // P0.06, PIN_BUTTON1 + 7, // P0.07, LORA_BUSY + 8, // P0.08, GPS_RESET + 9, // P0.09, PIN_RTC_EN + 10, // P0.10 + 11, // P0.11, PIN_SPI_SCK + 12, // P0.12, PIN_SPI_NSS + 13, // P0.13, PIN_SERIAL1_TX + 14, // P0.14, PIN_SERIAL1_RX + 15, // P0.15 + 16, // P0.16, PIN_SERIAL2_TX + 17, // P0.17, PIN_SERIAL2_RX + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24, LED_GREEN + 25, // P0.25, BUZZER_PIN + 26, // P0.26 + 27, // P0.27 + 28, // P0.28, LED_BLUE + 29, // P0.29, GPS_RTC_INT + 30, // P0.30, GPS_SLEEP_INT + 31, // P0.31 + 32, // P1.00 + 33, // P1.01, LORA_DIO_1 (LR2021 DIO8 IRQ) + 34, // P1.02 + 35, // P1.03, EXT_CHRG_DETECT + 36, // P1.04, CHARGE_DONE + 37, // P1.05, PIN_DRV_EN + 38, // P1.06, PIN_BAT_ADC_EN + 39, // P1.07, PIN_3V3_EN + 40, // P1.08, PIN_SPI_MISO + 41, // P1.09, PIN_SPI_MOSI + 42, // P1.10, LORA_RESET + 43, // P1.11, GPS_EN + 44, // P1.12 + 45, // P1.13, GPS_VRTC_EN + 46, // P1.14, PIN_WIRE_SCL + 47, // P1.15, PIN_WIRE_SDA + 255, // NRFX_SPIM_PIN_NOT_USED +}; + +void initVariant() +{ + pinMode(BATTERY_PIN, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, INPUT); + pinMode(PIN_BUTTON1, INPUT_PULLDOWN); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + + pinMode(PIN_BAT_ADC_EN, OUTPUT); + digitalWrite(PIN_BAT_ADC_EN, HIGH); + + pinMode(PIN_RTC_EN, OUTPUT); + digitalWrite(PIN_RTC_EN, LOW); + + pinMode(PIN_DRV_EN, OUTPUT); + digitalWrite(PIN_DRV_EN, LOW); + + pinMode(LED_RED, OUTPUT); + digitalWrite(LED_RED, LOW); + + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, LOW); + + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, LOW); + + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); + + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + + pinMode(GPS_RESET, OUTPUT); + digitalWrite(GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + + pinMode(BUZZER_PIN, INPUT_PULLDOWN); +} diff --git a/variants/meshtracker_x1/variant.h b/variants/meshtracker_x1/variant.h new file mode 100644 index 000000000..8e9bae8fa --- /dev/null +++ b/variants/meshtracker_x1/variant.h @@ -0,0 +1,123 @@ +/* + * variant.h - Seeed SenseCAP MeshTracker X1 (nRF52840 + Semtech LR2021) + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power +#define PIN_3V3_EN (39) // P1.7 Power to sensors +#define PIN_BAT_ADC_EN (38) // P1.6 Power to battery ADC divider +#define PIN_RTC_EN (9) // P0.9 Power to RTC + +#define BATTERY_PIN (2) // P0.2/AIN0 +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) + +#define EXT_CHRG_DETECT (35) // P1.3, LOW while charging +#define EXT_PWR_DETECT (5) // P0.5 + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (3.0) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (14) // P0.14 (GPS) +#define PIN_SERIAL1_TX (13) // P0.13 (GPS) + +#define PIN_SERIAL2_RX (17) // P0.17 +#define PIN_SERIAL2_TX (16) // P0.16 + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (47) // P1.15 +#define PIN_WIRE_SCL (46) // P1.14 +#define I2C_NO_RESCAN + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (40) // P1.8 +#define PIN_SPI_MOSI (41) // P1.9 +#define PIN_SPI_SCK (11) // P0.11 +#define PIN_SPI_NSS (12) // P0.12 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs (RGB) + +#define LED_BUILTIN (-1) +#define LED_RED (3) // P0.3 +#define LED_GREEN (24) // P0.24 +#define LED_BLUE (28) // P0.28 +#define LED_PIN LED_GREEN + +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) // P0.6, active HIGH (internal pull-down) +#define BUTTON_PIN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// LR2021 + +#define LORA_DIO_1 (33) // P1.1, IRQ line (LR2021 DIO8) +#define LORA_NSS (PIN_SPI_NSS) // P0.12 +#define LORA_RESET (42) // P1.10 +#define LORA_BUSY (7) // P0.7 +#define LORA_SCLK (PIN_SPI_SCK) // P0.11 +#define LORA_MISO (PIN_SPI_MISO) // P1.8 +#define LORA_MOSI (PIN_SPI_MOSI) // P1.9 + +//////////////////////////////////////////////////////////////////////////////// +// GPS (Airoha, dual-band L1+L5) + +#define HAS_GPS 1 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX +#define GPS_BAUD_RATE (115200) + +#define GPS_EN (43) // P1.11, active HIGH +#define GPS_RESET (8) // P0.8, active HIGH + +#define GPS_VRTC_EN (45) // P1.13 +#define GPS_SLEEP_INT (30) // P0.30 +#define GPS_RTC_INT (29) // P0.29 + +//////////////////////////////////////////////////////////////////////////////// +// Haptic driver (DRV2605 on I2C) + +#define PIN_DRV_EN (37) // P1.5, power to haptic driver + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_PIN (25) // P0.25, pwm output From e4fbd386afa359d62ef3aea06d0c7a38476369f1 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky <recrof@gmail.com> Date: Thu, 6 Aug 2026 12:37:21 +0200 Subject: [PATCH 184/191] ThinkNode M6: Remove QSPIFLASH=1 build flag to fix companion roles M6 doesn't have flash chip available, so companion roles fail to start. --- variants/thinknode_m6/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 3606d6a87..ad7e6902c 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -85,7 +85,6 @@ build_flags = -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 - -D QSPIFLASH=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ThinkNode_M6.build_src_filter} @@ -107,7 +106,6 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 - -D QSPIFLASH=1 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -D ENABLE_USB_INTERFACE From dee133d647b8f30745761aa45a347c1b47944322 Mon Sep 17 00:00:00 2001 From: liamcottle <liam@liamcottle.com> Date: Fri, 7 Aug 2026 00:19:18 +1200 Subject: [PATCH 185/191] disable cad on companion until it's configurable --- 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 b8661cafc..b03334c1c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -262,7 +262,7 @@ int MyMesh::getInterferenceThreshold() const { 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) + return false; // hardware CAD before TX (disabled by default, until configurable) } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { From 3abe415cf7c2623b7b4eb6951d525d89111361f3 Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:03:44 -0500 Subject: [PATCH 186/191] Add RGB LED and haptic notifications for SenseCAP MeshTracker X1 --- examples/companion_radio/NodePrefs.h | 2 + examples/companion_radio/ui-orig/UITask.cpp | 124 +++++++++++++++++++- examples/companion_radio/ui-orig/UITask.h | 7 ++ src/helpers/ui/DRV2605Vibration.cpp | 47 ++++++++ src/helpers/ui/DRV2605Vibration.h | 39 ++++++ variants/meshtracker_x1/platformio.ini | 11 ++ 6 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 src/helpers/ui/DRV2605Vibration.cpp create mode 100644 src/helpers/ui/DRV2605Vibration.h diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 39a5386a9..4ec1803e1 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -28,6 +28,7 @@ public: uint32_t ble_pin = 0; uint8_t advert_loc_policy = 0; uint8_t buzzer_quiet = 0; + uint8_t vibe_quiet = 0; uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled) uint32_t gps_interval = 0; // GPS read interval in seconds uint8_t autoadd_config = 0; // bitmask for auto-add contacts config @@ -101,6 +102,7 @@ private: def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key)); def("pin", _parent->ble_pin); def("buzz_q", _parent->buzzer_quiet); + def("vibe_q", _parent->vibe_quiet); def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config def("man_add", _parent->manual_add_contacts); def("tel_base", _parent->telemetry_mode_base); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 09fc8e770..08fc9b7ab 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -6,12 +6,19 @@ #define AUTO_OFF_MILLIS 15000 // 15 seconds #define BOOT_SCREEN_MILLIS 3000 // 3 seconds -#ifdef PIN_STATUS_LED +#if defined(PIN_STATUS_LED) || defined(PIN_STATUS_LED_R) #define LED_ON_MILLIS 20 #define LED_ON_MSG_MILLIS 200 #define LED_CYCLE_MILLIS 4000 #endif +#if defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STATUS_LED_B) +#define STATUS_LED_RGB 1 +#ifndef LOW_BATT_MILLIVOLTS +#define LOW_BATT_MILLIVOLTS 3500 +#endif +#endif + #ifndef USER_BTN_PRESSED #define USER_BTN_PRESSED LOW #endif @@ -60,6 +67,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no buzzer.startup(); #endif +#ifdef HAS_DRV2605 + vibration.begin(); + vibration.quiet(_node_prefs->vibe_quiet); +#endif + // Initialize digital button if available #ifdef PIN_USER_BTN _userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED); @@ -130,6 +142,10 @@ void UITask::clearMsgPreview() { void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { _msgcount = msgcount; +#ifdef HAS_DRV2605 + vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) +#endif + if (path_len == 0xFF) { sprintf(_origin, "(F) %s", from_name); } else { @@ -260,8 +276,88 @@ void UITask::renderCurrScreen() { _need_refresh = false; } +#ifdef STATUS_LED_RGB +static void statusLedWrite(uint8_t r, uint8_t g, uint8_t b) { + analogWrite(PIN_STATUS_LED_R, r); + analogWrite(PIN_STATUS_LED_G, g); + analogWrite(PIN_STATUS_LED_B, b); +} + +static void statusLedWheel(uint8_t pos) { // smooth hue sweep, 0..255 + if (pos < 85) { + statusLedWrite(255 - pos * 3, pos * 3, 0); + } else if (pos < 170) { + pos -= 85; + statusLedWrite(0, 255 - pos * 3, pos * 3); + } else { + pos -= 170; + statusLedWrite(pos * 3, 0, 255 - pos * 3); + } +} +#endif + void UITask::userLedHandler() { -#ifdef PIN_STATUS_LED +#ifdef STATUS_LED_RGB + static bool booted = false; + static bool flourish_active = false; + static unsigned long flourish_until = 0; + static int prev_msgcount = 0; + static int state = 0; + static unsigned long next_change = 0; + static int last_increment = 0; + static unsigned long next_batt_check = 0; + static bool low_batt = false; + + unsigned long cur_time = millis(); + + if (!booted) { // color sweep on boot + booted = true; + flourish_until = cur_time + 1200; + flourish_active = true; + } + if (_msgcount > prev_msgcount) { // color sweep when a new message arrives + flourish_until = cur_time + 800; + flourish_active = true; + } + prev_msgcount = _msgcount; + + if (flourish_active) { + if (cur_time < flourish_until) { + statusLedWheel((cur_time % 1200) * 255 / 1200); + return; + } + statusLedWrite(0, 0, 0); + flourish_active = false; + state = 0; + next_change = cur_time; + } + + if (cur_time > next_batt_check) { // battery reads are not free, keep them rare + low_batt = _board->getBattMilliVolts() < LOW_BATT_MILLIVOLTS; + next_batt_check = cur_time + 60000; + } + + if (cur_time > next_change) { + if (state == 0) { + state = 1; + last_increment = (_msgcount > 0) ? LED_ON_MSG_MILLIS : LED_ON_MILLIS; + next_change = cur_time + last_increment; + if (low_batt) { + statusLedWrite(255, 0, 0); // red: battery low + } else if (_msgcount > 0) { + statusLedWrite(255, 90, 0); // amber: unread messages + } else if (_connected) { + statusLedWrite(0, 0, 255); // blue: app connected + } else { + statusLedWrite(0, 255, 0); // green: heartbeat + } + } else { + state = 0; + next_change = cur_time + LED_CYCLE_MILLIS - last_increment; + statusLedWrite(0, 0, 0); + } + } +#elif defined(PIN_STATUS_LED) static int state = 0; static int next_change = 0; static int last_increment = 0; @@ -406,8 +502,26 @@ void UITask::handleButtonDoublePress() { void UITask::handleButtonTriplePress() { MESH_DEBUG_PRINTLN("UITask: triple press triggered"); - // Toggle buzzer quiet mode - #ifdef PIN_BUZZER +#if defined(PIN_BUZZER) && defined(HAS_DRV2605) + // cycle alert modes: Buzz+Vibe -> Buzz only -> Vibe only -> Silent + int mode = (_node_prefs->buzzer_quiet ? 2 : 0) | (_node_prefs->vibe_quiet ? 1 : 0); + mode = (mode + 1) & 3; + _node_prefs->buzzer_quiet = (mode & 2) ? 1 : 0; + _node_prefs->vibe_quiet = (mode & 1) ? 1 : 0; + buzzer.quiet(_node_prefs->buzzer_quiet); + vibration.quiet(_node_prefs->vibe_quiet); + // audible/tactile confirmation of the new mode (no screen on some boards) + if (!_node_prefs->buzzer_quiet) notify(UIEventType::ack); + if (!_node_prefs->vibe_quiet) vibration.trigger(true); + switch (mode) { + case 0: sprintf(_alert, "Alerts: Buzz+Vibe"); break; + case 1: sprintf(_alert, "Alerts: Buzz only"); break; + case 2: sprintf(_alert, "Alerts: Vibe only"); break; + default: sprintf(_alert, "Alerts: Silent"); break; + } + the_mesh.savePrefs(); + _need_refresh = true; +#elif defined(PIN_BUZZER) if (buzzer.isQuiet()) { buzzer.quiet(false); notify(UIEventType::ack); @@ -419,7 +533,7 @@ void UITask::handleButtonTriplePress() { _node_prefs->buzzer_quiet = buzzer.isQuiet(); the_mesh.savePrefs(); _need_refresh = true; - #endif +#endif } void UITask::handleButtonQuadruplePress() { diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 961c07a03..02d126e8f 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -14,11 +14,18 @@ #include "Button.h" +#ifdef HAS_DRV2605 + #include <helpers/ui/DRV2605Vibration.h> +#endif + class UITask : public AbstractUITask { DisplayDriver* _display; SensorManager* _sensors; #ifdef PIN_BUZZER genericBuzzer buzzer; +#endif +#ifdef HAS_DRV2605 + DRV2605Vibration vibration; #endif unsigned long _next_refresh, _auto_off; NodePrefs* _node_prefs; diff --git a/src/helpers/ui/DRV2605Vibration.cpp b/src/helpers/ui/DRV2605Vibration.cpp new file mode 100644 index 000000000..3a42bb884 --- /dev/null +++ b/src/helpers/ui/DRV2605Vibration.cpp @@ -0,0 +1,47 @@ +#ifdef HAS_DRV2605 + +#include "DRV2605Vibration.h" +#include <Wire.h> + +void DRV2605Vibration::begin() { +#ifdef PIN_DRV_EN + pinMode(PIN_DRV_EN, OUTPUT); + digitalWrite(PIN_DRV_EN, HIGH); // power up the haptic driver + delay(10); +#endif + if (!drv.begin(&Wire)) { + return; // no haptic driver found, stay silent + } +#ifdef DRV2605_USE_LRA + // LRA mode: 4x brake factor, medium loop gain, back-EMF gain 2 + drv.writeRegister8(DRV2605_REG_FEEDBACK, 0xB6); +#endif + drv.selectLibrary(1); + drv.setMode(DRV2605_MODE_INTTRIG); + _ready = true; +} + +void DRV2605Vibration::trigger(bool force) { + if (!_ready || _quiet) return; + unsigned long now = millis(); + if (!force && _last_trigger != 0 && now - _last_trigger < VIBRATION_TIMEOUT) return; + _last_trigger = now; + drv.setWaveform(0, DRV2605_EFFECT); + drv.setWaveform(1, 0); // pause + drv.setWaveform(2, DRV2605_EFFECT); + drv.setWaveform(3, 0); // end of sequence + drv.go(); +} + +void DRV2605Vibration::loop() { +} + +bool DRV2605Vibration::isVibrating() { + return false; // effects are short, treat as instantaneous +} + +void DRV2605Vibration::stop() { + if (_ready) drv.stop(); +} + +#endif // ifdef HAS_DRV2605 diff --git a/src/helpers/ui/DRV2605Vibration.h b/src/helpers/ui/DRV2605Vibration.h new file mode 100644 index 000000000..a40894be1 --- /dev/null +++ b/src/helpers/ui/DRV2605Vibration.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef HAS_DRV2605 + +#include <Arduino.h> +#include <Adafruit_DRV2605.h> + +/* + * Vibration control class for boards where the motor is behind a + * DRV2605 haptic driver on I2C (e.g. Seeed SenseCAP MeshTracker X1). + * Same interface as GenericVibration. + */ + +#ifndef VIBRATION_TIMEOUT +#define VIBRATION_TIMEOUT 5000 // cooldown between vibrations +#endif + +#ifndef DRV2605_EFFECT +#define DRV2605_EFFECT 16 // "1000 ms alert" from the DRV2605 effect library +#endif + +class DRV2605Vibration { +public: + void begin(); // power up and init the DRV2605 + void trigger(bool force = false); // trigger vibration; force skips the cooldown + void loop(); // no-op, the DRV2605 plays effects autonomously + bool isVibrating(); + void stop(); // stop vibration immediately + void quiet(bool q) { _quiet = q; } + bool isQuiet() const { return _quiet; } + +private: + Adafruit_DRV2605 drv; + bool _ready = false; + bool _quiet = false; + unsigned long _last_trigger = 0; +}; + +#endif // ifdef HAS_DRV2605 diff --git a/variants/meshtracker_x1/platformio.ini b/variants/meshtracker_x1/platformio.ini index dbab5ca63..185069168 100644 --- a/variants/meshtracker_x1/platformio.ini +++ b/variants/meshtracker_x1/platformio.ini @@ -12,6 +12,9 @@ build_flags = ${nrf52_base.build_flags} -D PIN_USER_BTN=6 -D USER_BTN_PRESSED=HIGH -D PIN_STATUS_LED=24 + -D PIN_STATUS_LED_R=3 ; P0.3 red + -D PIN_STATUS_LED_G=24 ; P0.24 green + -D PIN_STATUS_LED_B=28 ; P0.28 blue -D RADIO_CLASS=CustomLR2021 -D USE_LR2021 -D WRAPPER_CLASS=CustomLR2021Wrapper @@ -77,16 +80,20 @@ build_flags = ${MeshTracker_X1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=25 + -D HAS_DRV2605=1 + -D DRV2605_USE_LRA=1 -D ENABLE_USB_INTERFACE build_src_filter = ${MeshTracker_X1.build_src_filter} +<helpers/ui/buzzer.cpp> +<helpers/ui/NullDisplayDriver.cpp> + +<helpers/ui/DRV2605Vibration.cpp> +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${MeshTracker_X1.lib_deps} densaugeo/base64 @ ~1.4.0 stevemarple/MicroNMEA @ ^2.0.6 end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit DRV2605 Library @ ^1.2.4 [env:MeshTracker_X1_companion_radio_ble] extends = MeshTracker_X1 @@ -104,14 +111,18 @@ build_flags = ${MeshTracker_X1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=25 + -D HAS_DRV2605=1 + -D DRV2605_USE_LRA=1 -D ADVERT_NAME='"@@MAC"' build_src_filter = ${MeshTracker_X1.build_src_filter} +<helpers/nrf52/SerialBLEInterface.cpp> +<helpers/ui/buzzer.cpp> +<helpers/ui/NullDisplayDriver.cpp> + +<helpers/ui/DRV2605Vibration.cpp> +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${MeshTracker_X1.lib_deps} densaugeo/base64 @ ~1.4.0 stevemarple/MicroNMEA @ ^2.0.6 end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit DRV2605 Library @ ^1.2.4 From 335ebf546d6f1f15c0bf4dbce08dd8a65d1ceb2c Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:34:02 -0500 Subject: [PATCH 187/191] Add SPA06 barometer telemetry and charge status LED for MeshTracker X1 --- examples/companion_radio/ui-orig/UITask.cpp | 30 +++++++++++++++++++++ variants/meshtracker_x1/platformio.ini | 2 ++ variants/meshtracker_x1/target.cpp | 16 +++++++++++ variants/meshtracker_x1/target.h | 3 +++ variants/meshtracker_x1/variant.cpp | 3 ++- variants/meshtracker_x1/variant.h | 1 + 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 08fc9b7ab..1e9613165 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -337,6 +337,36 @@ void UITask::userLedHandler() { next_batt_check = cur_time + 60000; } +#ifdef EXT_CHRG_DETECT + // charge status display while docked (skipped when messages are waiting, + // so the unread indication is never masked) + static unsigned long next_pwr_check = 0; + static bool ext_powered = false, ext_charging = false; + if (cur_time > next_pwr_check) { + ext_powered = _board->isExternalPowered(); + bool chrg = digitalRead(EXT_CHRG_DETECT) == LOW; + #ifdef EXT_CHRG_DONE + if (digitalRead(EXT_CHRG_DONE) == LOW) chrg = false; // charge-done wins + #endif + ext_charging = ext_powered && chrg; + next_pwr_check = cur_time + 1000; + } + if (ext_powered && _msgcount == 0) { + if (ext_charging) { + // amber breathing while charging + int ph = cur_time % 2000; + int v = ph < 1000 ? ph : 2000 - ph; + uint8_t lvl = (uint8_t)(v * 255 / 1000); + statusLedWrite(lvl, (uint8_t)(lvl * 35 / 100), 0); + } else { + statusLedWrite(0, 40, 0); // dim solid green: charge complete + } + state = 0; + next_change = cur_time; + return; + } +#endif + if (cur_time > next_change) { if (state == 0) { state = 1; diff --git a/variants/meshtracker_x1/platformio.ini b/variants/meshtracker_x1/platformio.ini index 185069168..6a46a79f9 100644 --- a/variants/meshtracker_x1/platformio.ini +++ b/variants/meshtracker_x1/platformio.ini @@ -32,6 +32,8 @@ build_flags = ${nrf52_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<helpers/*.cpp> +<../variants/meshtracker_x1> +lib_deps = ${nrf52_base.lib_deps} + https://github.com/adafruit/Adafruit_SPA06_003/archive/refs/tags/1.0.2.zip debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/meshtracker_x1/target.cpp b/variants/meshtracker_x1/target.cpp index 1139b8b71..d6d548b51 100644 --- a/variants/meshtracker_x1/target.cpp +++ b/variants/meshtracker_x1/target.cpp @@ -67,6 +67,17 @@ void MeshTrackerX1SensorManager::stop_gps() { bool MeshTrackerX1SensorManager::begin() { // init GPS Serial1.begin(GPS_BAUD_RATE); + + // init SPA06-003 barometer + baro_ok = spa06.begin(SPA06_003_DEFAULT_ADDR, &Wire) || spa06.begin(0x76, &Wire); + if (baro_ok) { + spa06.setPressureOversampling(SPA06_003_OVERSAMPLE_8); + spa06.setTemperatureOversampling(SPA06_003_OVERSAMPLE_8); + // 1 Hz continuous keeps reads non-blocking at minimal power cost + spa06.setPressureMeasureRate(SPA06_003_RATE_1); + spa06.setTemperatureMeasureRate(SPA06_003_RATE_1); + spa06.setMeasurementMode(SPA06_003_MEAS_CONTINUOUS_BOTH); + } return true; } @@ -74,12 +85,17 @@ bool MeshTrackerX1SensorManager::querySensors(uint8_t requester_permissions, Cay if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); } + if (requester_permissions & TELEM_PERM_ENVIRONMENT && baro_ok) { + telemetry.addTemperature(TELEM_CHANNEL_SELF, spa06.readTemperature()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, spa06.readPressure()); + } return true; } void MeshTrackerX1SensorManager::loop() { static long next_gps_update = 0; + _nmea->loop(); if (millis() > next_gps_update) { diff --git a/variants/meshtracker_x1/target.h b/variants/meshtracker_x1/target.h index 665f43281..9741efb6a 100644 --- a/variants/meshtracker_x1/target.h +++ b/variants/meshtracker_x1/target.h @@ -8,12 +8,15 @@ #include <helpers/ArduinoHelpers.h> #include <helpers/SensorManager.h> #include <helpers/sensors/LocationProvider.h> +#include <Adafruit_SPA06_003.h> #ifdef DISPLAY_CLASS #include "NullDisplayDriver.h" #endif class MeshTrackerX1SensorManager: public SensorManager { bool gps_active = false; + bool baro_ok = false; + Adafruit_SPA06_003 spa06; LocationProvider * _nmea; void start_gps(); diff --git a/variants/meshtracker_x1/variant.cpp b/variants/meshtracker_x1/variant.cpp index a0e643801..5a685a8ef 100644 --- a/variants/meshtracker_x1/variant.cpp +++ b/variants/meshtracker_x1/variant.cpp @@ -62,7 +62,8 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = void initVariant() { pinMode(BATTERY_PIN, INPUT); - pinMode(EXT_CHRG_DETECT, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT_PULLUP); + pinMode(EXT_CHRG_DONE, INPUT_PULLUP); pinMode(EXT_PWR_DETECT, INPUT); pinMode(PIN_BUTTON1, INPUT_PULLDOWN); diff --git a/variants/meshtracker_x1/variant.h b/variants/meshtracker_x1/variant.h index 8e9bae8fa..c3dbd274f 100644 --- a/variants/meshtracker_x1/variant.h +++ b/variants/meshtracker_x1/variant.h @@ -25,6 +25,7 @@ #define ADC_MULTIPLIER (2.0F) #define EXT_CHRG_DETECT (35) // P1.3, LOW while charging +#define EXT_CHRG_DONE (36) // P1.4, LOW when charge complete #define EXT_PWR_DETECT (5) // P0.5 #define ADC_RESOLUTION (14) From 2af4b3c14ac194dc614e575182a9e850f7cf7f68 Mon Sep 17 00:00:00 2001 From: Hacuchino-hash <246103064+Hacuchino-hash@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:08:08 -0500 Subject: [PATCH 188/191] Fix X1 charge status LED always showing charged --- examples/companion_radio/ui-orig/UITask.cpp | 6 +----- variants/meshtracker_x1/variant.cpp | 1 - variants/meshtracker_x1/variant.h | 3 ++- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 1e9613165..b49b05dd1 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -344,11 +344,7 @@ void UITask::userLedHandler() { static bool ext_powered = false, ext_charging = false; if (cur_time > next_pwr_check) { ext_powered = _board->isExternalPowered(); - bool chrg = digitalRead(EXT_CHRG_DETECT) == LOW; - #ifdef EXT_CHRG_DONE - if (digitalRead(EXT_CHRG_DONE) == LOW) chrg = false; // charge-done wins - #endif - ext_charging = ext_powered && chrg; + ext_charging = ext_powered && digitalRead(EXT_CHRG_DETECT) == LOW; next_pwr_check = cur_time + 1000; } if (ext_powered && _msgcount == 0) { diff --git a/variants/meshtracker_x1/variant.cpp b/variants/meshtracker_x1/variant.cpp index 5a685a8ef..49cbc8dc4 100644 --- a/variants/meshtracker_x1/variant.cpp +++ b/variants/meshtracker_x1/variant.cpp @@ -63,7 +63,6 @@ void initVariant() { pinMode(BATTERY_PIN, INPUT); pinMode(EXT_CHRG_DETECT, INPUT_PULLUP); - pinMode(EXT_CHRG_DONE, INPUT_PULLUP); pinMode(EXT_PWR_DETECT, INPUT); pinMode(PIN_BUTTON1, INPUT_PULLDOWN); diff --git a/variants/meshtracker_x1/variant.h b/variants/meshtracker_x1/variant.h index c3dbd274f..7672044c2 100644 --- a/variants/meshtracker_x1/variant.h +++ b/variants/meshtracker_x1/variant.h @@ -25,7 +25,8 @@ #define ADC_MULTIPLIER (2.0F) #define EXT_CHRG_DETECT (35) // P1.3, LOW while charging -#define EXT_CHRG_DONE (36) // P1.4, LOW when charge complete +// P1.4 is the charger's second status line, but it reads LOW regardless of +// charge state on this board, so it is not used (Meshtastic leaves it out too) #define EXT_PWR_DETECT (5) // P0.5 #define ADC_RESOLUTION (14) From 02ec123eb820f9dc14450511eb317764b99c34f4 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Sun, 9 Aug 2026 11:41:09 +1000 Subject: [PATCH 189/191] add QSPIFlash support for Seeed MeshTracker X1 --- variants/meshtracker_x1/platformio.ini | 2 ++ variants/meshtracker_x1/variant.cpp | 3 +++ variants/meshtracker_x1/variant.h | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/variants/meshtracker_x1/platformio.ini b/variants/meshtracker_x1/platformio.ini index 6a46a79f9..1ce8bf17a 100644 --- a/variants/meshtracker_x1/platformio.ini +++ b/variants/meshtracker_x1/platformio.ini @@ -75,6 +75,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${MeshTracker_X1.build_flags} -I examples/companion_radio/ui-orig + -D QSPIFLASH=1 -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 @@ -103,6 +104,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${MeshTracker_X1.build_flags} -I examples/companion_radio/ui-orig + -D QSPIFLASH=1 -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 diff --git a/variants/meshtracker_x1/variant.cpp b/variants/meshtracker_x1/variant.cpp index 5a685a8ef..27c4c22ef 100644 --- a/variants/meshtracker_x1/variant.cpp +++ b/variants/meshtracker_x1/variant.cpp @@ -70,6 +70,9 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_FLASH_EN, OUTPUT); + digitalWrite(PIN_FLASH_EN, HIGH); + pinMode(PIN_BAT_ADC_EN, OUTPUT); digitalWrite(PIN_BAT_ADC_EN, HIGH); diff --git a/variants/meshtracker_x1/variant.h b/variants/meshtracker_x1/variant.h index c3dbd274f..46414b488 100644 --- a/variants/meshtracker_x1/variant.h +++ b/variants/meshtracker_x1/variant.h @@ -122,3 +122,17 @@ // Buzzer #define BUZZER_PIN (25) // P0.25, pwm output + +//////////////////////////////////////////////////////////////////////////////// +// QSPI Flash + +#define PIN_FLASH_EN (15) // P0.15 (Flash power enable) + +#define PIN_QSPI_SCK (19) // P0.19 +#define PIN_QSPI_CS (20) // P0.20 +#define PIN_QSPI_IO0 (21) // P0.21 +#define PIN_QSPI_IO1 (22) // P0.22 +#define PIN_QSPI_IO2 (23) // P0.23 +#define PIN_QSPI_IO3 (32) // P1.00 + +#define EXTERNAL_FLASH_DEVICES GD25Q64C \ No newline at end of file From 9420195626da0ec03d4603b26b12504736627e7e Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Sun, 9 Aug 2026 11:45:35 +1000 Subject: [PATCH 190/191] bump CustomLFS version for GD25Q64C support --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index aa336dec7..e78124a40 100644 --- a/platformio.ini +++ b/platformio.ini @@ -94,7 +94,7 @@ build_flags = ${arduino_base.build_flags} -D USE_CC310_HW_CRYPTO=1 lib_deps = ${arduino_base.lib_deps} - https://github.com/oltaco/CustomLFS#0.2.2 + https://github.com/oltaco/CustomLFS#0.2.3 ; ----------------- RP2040 --------------------- [rp2040_base] From 93b2db63de9009af047a2d0e1bcd97b4b0ba7106 Mon Sep 17 00:00:00 2001 From: Scott Powell <ripple_biz@protonmail.com> Date: Sun, 9 Aug 2026 15:14:46 +1000 Subject: [PATCH 191/191] * version 1.17.0 --- examples/companion_radio/MyMesh.h | 4 ++-- examples/simple_repeater/MyMesh.h | 4 ++-- examples/simple_room_server/MyMesh.h | 4 ++-- examples/simple_sensor/SensorMesh.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index d95b073fb..f73fe0e06 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 13 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "6 Jun 2026" +#define FIRMWARE_BUILD_DATE "9 Aug 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.16.0" +#define FIRMWARE_VERSION "v1.17.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index e959f01a7..2f923c871 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -70,11 +70,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "6 Jun 2026" + #define FIRMWARE_BUILD_DATE "9 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5f78bee81..878c2e959 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -27,11 +27,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "6 Jun 2026" + #define FIRMWARE_BUILD_DATE "9 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 1d65b8772..3a33b638c 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -34,11 +34,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "6 Jun 2026" + #define FIRMWARE_BUILD_DATE "9 Aug 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #define FIRMWARE_ROLE "sensor"