From 9c9a2ba2db593b203eb66a428fd21f9b26a7d24e Mon Sep 17 00:00:00 2001 From: Daniel Novak Date: Mon, 23 Feb 2026 21:40:17 +0100 Subject: [PATCH 001/174] Fix stack buffer overflow in MACThenDecrypt via block-alignment check Add validation that ciphertext length is a multiple of CIPHER_BLOCK_SIZE before calling decrypt(). The decrypt() function processes data in 16-byte AES blocks, rounding up non-aligned input, which can write up to 8 bytes past the caller's stack buffer. This can be triggered by crafted packets that pass the 2-byte HMAC check (1-in-65536 chance per attempt), enabling a persistent denial-of-service attack against repeaters and other nodes. --- src/Utils.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720..573c4398 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -82,7 +82,9 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); } if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { - return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + int enc_len = src_len - CIPHER_MAC_SIZE; + if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext + return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len); } return 0; // invalid HMAC } From b74ba545308f077120cf329f2023f171e2e2f9fa Mon Sep 17 00:00:00 2001 From: Daniel Novak Date: Thu, 26 Feb 2026 09:08:06 +0100 Subject: [PATCH 002/174] Move block-alignment check before HMAC computation Address review feedback: - Move the alignment check before the HMAC-SHA256 computation to skip expensive crypto on obviously malformed packets - Update MACThenDecrypt header comment to document block-alignment as a reason for returning zero --- src/Utils.cpp | 7 ++++--- src/Utils.h | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index 573c4398..a70d4a70 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -74,16 +74,17 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes + int enc_len = src_len - CIPHER_MAC_SIZE; + if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext + uint8_t hmac[CIPHER_MAC_SIZE]; { SHA256 sha; sha.resetHMAC(shared_secret, PUB_KEY_SIZE); - sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + sha.update(src + CIPHER_MAC_SIZE, enc_len); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); } if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { - int enc_len = src_len - CIPHER_MAC_SIZE; - if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len); } return 0; // invalid HMAC diff --git a/src/Utils.h b/src/Utils.h index 5736b874..9997fe3c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -50,7 +50,7 @@ public: /** * \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src. - * \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest' + * \returns zero if MAC is invalid or ciphertext is not block-aligned, otherwise the length of decrypted bytes in 'dest' */ static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); From e525200ce1f88044167d1431719c9ebc5a69ecfd Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Tue, 17 Mar 2026 00:50:19 -0500 Subject: [PATCH 003/174] Fix T1000-E sensor power pin mapping --- variants/t1000-e/T1000eBoard.h | 4 ---- variants/t1000-e/t1000e_sensors.cpp | 4 ---- variants/t1000-e/variant.cpp | 4 +--- variants/t1000-e/variant.h | 3 +-- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index 49223607..546bd774 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -69,10 +69,6 @@ public: #ifdef PIN_3V3_ACC_EN digitalWrite(PIN_3V3_ACC_EN, LOW); #endif - #ifdef SENSOR_EN - digitalWrite(SENSOR_EN, LOW); - #endif - // set led on and wait for button release before poweroff #ifdef LED_PIN digitalWrite(LED_PIN, HIGH); diff --git a/variants/t1000-e/t1000e_sensors.cpp b/variants/t1000-e/t1000e_sensors.cpp index 85298d3a..cb4977fd 100644 --- a/variants/t1000-e/t1000e_sensors.cpp +++ b/variants/t1000-e/t1000e_sensors.cpp @@ -72,7 +72,6 @@ float t1000e_get_temperature(void) { unsigned int ntc_v, vcc_v; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); @@ -80,7 +79,6 @@ float t1000e_get_temperature(void) { vcc_v = (rail_v > NTC_REF_VCC) ? NTC_REF_VCC : rail_v; ntc_v = (1000.0 * AREF_VOLTAGE * analogRead(TEMP_SENSOR)) / 4096; digitalWrite(PIN_3V3_EN, LOW); - digitalWrite(SENSOR_EN, LOW); return get_heater_temperature(vcc_v, ntc_v); } @@ -90,13 +88,11 @@ uint32_t t1000e_get_light(void) { unsigned int lux_v = 0; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); lux_v = 1000 * analogRead(LUX_SENSOR) * AREF_VOLTAGE / 4096; lux = get_light_lv(lux_v); - digitalWrite(SENSOR_EN, LOW); digitalWrite(PIN_3V3_EN, LOW); return lux; diff --git a/variants/t1000-e/variant.cpp b/variants/t1000-e/variant.cpp index a598e3ca..5627e659 100644 --- a/variants/t1000-e/variant.cpp +++ b/variants/t1000-e/variant.cpp @@ -14,7 +14,7 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 1, // P0.01 2, // P0.02, AIN0 BATTERY_PIN 3, // P0.03 - 4, // P0.04, SENSOR_EN + 4, // P0.04 5, // P0.05, EXT_PWR_DETEC 6, // P0.06, PIN_BUTTON1 7, // P0.07, LORA_BUSY @@ -77,7 +77,6 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); pinMode(PIN_3V3_ACC_EN, OUTPUT); pinMode(BUZZER_EN, OUTPUT); - pinMode(SENSOR_EN, OUTPUT); pinMode(GPS_EN, OUTPUT); pinMode(GPS_RESET, OUTPUT); pinMode(GPS_VRTC_EN, OUTPUT); @@ -88,7 +87,6 @@ void initVariant() digitalWrite(PIN_3V3_EN, LOW); digitalWrite(PIN_3V3_ACC_EN, LOW); digitalWrite(BUZZER_EN, LOW); - digitalWrite(SENSOR_EN, LOW); digitalWrite(GPS_EN, LOW); digitalWrite(GPS_RESET, LOW); digitalWrite(GPS_VRTC_EN, LOW); diff --git a/variants/t1000-e/variant.h b/variants/t1000-e/variant.h index 458f12bc..81e028d6 100644 --- a/variants/t1000-e/variant.h +++ b/variants/t1000-e/variant.h @@ -120,7 +120,6 @@ //////////////////////////////////////////////////////////////////////////////// // Temp+Lux Sensor -#define SENSOR_EN (4) // P0.4 #define TEMP_SENSOR (31) // P0.31/AIN7 #define LUX_SENSOR (29) // P0.29/AIN5 @@ -133,4 +132,4 @@ // 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 3984b872ca4a0b4c7bddb48e089e52e608f1dc3b Mon Sep 17 00:00:00 2001 From: Frieder Schrempf Date: Wed, 22 Apr 2026 19:56:21 +0200 Subject: [PATCH 004/174] Disable NRF52 undervoltage bootlock feature The NRF52 undervoltage bootlock feature implemented behind NRF52_POWER_MANAGEMENT was introduced in v1.12. It configures a fixed voltage level of 3.3V and prevents the software from booting if the battery voltage is below that level. This is problematic for various reasons. The most important one being that it breaks devices that use cell chemistries other than Li-Ion, such as LFP, LTO or SIB which have much lower minimum voltages. As a quick fix to prevent devices from failing, disable the feature until it has been decided how to handle this in the future and a proper implementation has been provided. Signed-off-by: Frieder Schrempf --- variants/gat562_30s_mesh_kit/platformio.ini | 1 - variants/gat562_mesh_evb_pro/platformio.ini | 1 - variants/gat562_mesh_tracker_pro/platformio.ini | 1 - variants/gat562_mesh_watch13/platformio.ini | 1 - variants/heltec_t096/platformio.ini | 1 - variants/heltec_t114/platformio.ini | 1 - variants/muziworks_r1_neo/platformio.ini | 1 - variants/rak3401/platformio.ini | 1 - variants/rak4631/platformio.ini | 1 - variants/sensecap_solar/platformio.ini | 1 - variants/xiao_nrf52/platformio.ini | 1 - 11 files changed, 11 deletions(-) diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 1467f0fa..efc87e37 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -7,7 +7,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index cede9c97..b14e89af 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,6 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 8a947bce..1f7502d6 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -5,7 +5,6 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro - -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index ef30829d..92bfe54c 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 19b05f3c..3d72c96b 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index b985030f..a6ad2a38 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 39ef8728..645961ce 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -7,7 +7,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/muziworks_r1_neo -I src/helpers/ui -D R1Neo - -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 3d2d4a3e..949bf2c4 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,7 +6,6 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 - -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index ea7e49c3..37365354 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -7,7 +7,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index aabbcf00..24407180 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,6 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=11 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a0854336..931629c7 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,6 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM - -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 From fc71d34509edf593dfd12622d8c0c8ac2394107d Mon Sep 17 00:00:00 2001 From: Emily Kerst Date: Sun, 3 May 2026 22:40:01 +0200 Subject: [PATCH 005/174] Added USB Companion variant for LilyGo T-Echo Lite Co-authored-by: Natalie Kagelmacher Mentored-by: Natalie Kagelmacher Tested-by: Emily Kerst --- variants/lilygo_techo_lite/platformio.ini | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 4b5edf69..db629b36 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -190,3 +190,24 @@ lib_deps = extends = LilyGo_T-Echo-Lite build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} +<../examples/kiss_modem/> +[env:LilyGo_T-Echo-Lite_companion_radio_usb] +extends = LilyGo_T-Echo-Lite +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${LilyGo_T-Echo-Lite.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D UI_RECENT_LIST_SIZE=9 + -D UI_SENSORS_PAGE=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 +; -D QSPIFLASH=1 +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T-Echo-Lite.lib_deps} + densaugeo/base64 @ ~1.4.0 From 356af9fdc8ff60a2cd60432b6aae2162f6ed07a0 Mon Sep 17 00:00:00 2001 From: Emily Kerst Date: Fri, 15 May 2026 20:20:27 +0200 Subject: [PATCH 006/174] chore: reformat T-Echo Lite platformio.ini to keep normal and non-shell variants together --- variants/lilygo_techo_lite/platformio.ini | 52 +++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index db629b36..ed46adcd 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -98,6 +98,32 @@ lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:LilyGo_T-Echo-Lite_kiss_modem] +extends = LilyGo_T-Echo-Lite +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/kiss_modem/> +[env:LilyGo_T-Echo-Lite_companion_radio_usb] +extends = LilyGo_T-Echo-Lite +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${LilyGo_T-Echo-Lite.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D UI_RECENT_LIST_SIZE=9 + -D UI_SENSORS_PAGE=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 +; -D QSPIFLASH=1 +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T-Echo-Lite.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:LilyGo_T-Echo-Lite_non_shell_companion_radio_ble] extends = LilyGo_T-Echo-Lite upload_protocol = nrfutil @@ -185,29 +211,3 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 - -[env:LilyGo_T-Echo-Lite_kiss_modem] -extends = LilyGo_T-Echo-Lite -build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} - +<../examples/kiss_modem/> -[env:LilyGo_T-Echo-Lite_companion_radio_usb] -extends = LilyGo_T-Echo-Lite -board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld -board_upload.maximum_size = 712704 -build_flags = - ${LilyGo_T-Echo-Lite.build_flags} - -I src/helpers/ui - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D OFFLINE_QUEUE_SIZE=256 - -D UI_RECENT_LIST_SIZE=9 - -D UI_SENSORS_PAGE=1 - -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -; -D QSPIFLASH=1 -build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} - +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = - ${LilyGo_T-Echo-Lite.lib_deps} - densaugeo/base64 @ ~1.4.0 From 27feb73d4ebf8ca4107f3b58bab854a8016bfa24 Mon Sep 17 00:00:00 2001 From: Jody Bentley Date: Wed, 24 Jun 2026 19:43:55 -0400 Subject: [PATCH 007/174] GPS: poll up to 5s for cold-start detection instead of a single 1s check initBasicGPS() waited a fixed 1 second and then decided the GPS was present only if NMEA data had already arrived. Cold-start GPS modules often need 2-5 seconds before they emit their first sentence, so on a cold boot detection frequently failed. A failed detection sets gps_detected = false, which hides all GPS CLI settings and disables GPS time sync until the next reboot - so the GPS appears permanently dead even though the hardware is fine. Poll for up to ~5 seconds (in 250ms steps) and return as soon as any data appears. Fast modules are detected almost immediately and add no boot delay; slow cold-start modules now get enough time. The ENV_SKIP_GPS_DETECT bypass is unchanged. Fixes #2258. --- src/helpers/sensors/EnvironmentSensorManager.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 73842d9e..b26fc038 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -748,14 +748,18 @@ void EnvironmentSensorManager::initBasicGPS() { MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on..."); #endif - // Give GPS a moment to power up and send data - delay(1000); - - // We'll consider GPS detected if we see any data on Serial1 + // Give the GPS time to power up and emit its first NMEA sentence. Cold-start + // modules can take several seconds, so poll for up to ~5s instead of checking + // once after a fixed 1s delay -- returning as soon as any data appears, so + // fast modules add no boot delay (issue #2258). #ifdef ENV_SKIP_GPS_DETECT gps_detected = true; #else - gps_detected = (Serial1.available() > 0); + gps_detected = false; + for (int i = 0; i < 20 && !gps_detected; i++) { + delay(250); + gps_detected = (Serial1.available() > 0); + } #endif if (gps_detected) { From 4866268cb9fabfcfdb406ff86521badfb841d497 Mon Sep 17 00:00:00 2001 From: me Date: Sun, 7 Jun 2026 18:11:23 -0700 Subject: [PATCH 008/174] fix(techo-lite): remove duplicate EXTERNAL_FLASH_DEVICES definition variant.h defines EXTERNAL_FLASH_DEVICES twice: - First (correct): ZD25WQ32CEIGR (32Mbit = 4MB, matches actual hardware) - Second (incorrect): MX25R1635F (16Mbit = 2MB, T-Echo non-Lite chip) Due to C preprocessor behavior, the second definition silently overrides the first, causing firmware to initialize the wrong flash chip. This can result in LittleFS operating incorrectly on T-Echo Lite hardware. Remove the erroneous second definition. Also fix missing newline at end of file. --- variants/lilygo_techo_lite/variant.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/variants/lilygo_techo_lite/variant.h b/variants/lilygo_techo_lite/variant.h index 702ab334..628ffe19 100644 --- a/variants/lilygo_techo_lite/variant.h +++ b/variants/lilygo_techo_lite/variant.h @@ -95,9 +95,6 @@ #define PIN_BUTTON2 _PINNUM(0, 18) #define BUTTON_PIN2 PIN_BUTTON2 -#define EXTERNAL_FLASH_DEVICES MX25R1635F -#define EXTERNAL_FLASH_USE_QSPI - //////////////////////////////////////////////////////////////////////////////// // Lora @@ -156,4 +153,4 @@ extern const int SCK; #define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX #define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable #define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up -#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS \ No newline at end of file +#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS From 5de6512090f657dca2c717347ce1cbf516fb6f77 Mon Sep 17 00:00:00 2001 From: me Date: Thu, 2 Jul 2026 08:43:06 -0700 Subject: [PATCH 009/174] fix(techo-lite): remove unused EXTERNAL_FLASH_DEVICES definition entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback from @liamcottle and @entr0p1, EXTERNAL_FLASH_DEVICES is not used by MeshCore firmware — CustomLFS determines the flash chip via JEDEC ID at runtime instead. Removing the definition entirely rather than keeping the (now-confirmed-unnecessary) correct value. Also removes EXTERNAL_FLASH_USE_QSPI, which was paired with the same define. --- variants/lilygo_techo_lite/variant.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/variants/lilygo_techo_lite/variant.h b/variants/lilygo_techo_lite/variant.h index 628ffe19..673e8fc7 100644 --- a/variants/lilygo_techo_lite/variant.h +++ b/variants/lilygo_techo_lite/variant.h @@ -70,9 +70,6 @@ #define PIN_QSPI_IO2 _PINNUM(1, 9) #define PIN_QSPI_IO3 _PINNUM(0, 26) -#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR -#define EXTERNAL_FLASH_USE_QSPI - //////////////////////////////////////////////////////////////////////////////// // Builtin LEDs From 67902026098b0fec8a3a8305d4d37493fdb4aab4 Mon Sep 17 00:00:00 2001 From: Szetya Date: Mon, 20 Jul 2026 14:11:06 +0200 Subject: [PATCH 010/174] Update variant.h Correct of GPS RX/TX Pins. --- variants/lilygo_techo_lite/variant.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/lilygo_techo_lite/variant.h b/variants/lilygo_techo_lite/variant.h index 702ab334..19b12b1b 100644 --- a/variants/lilygo_techo_lite/variant.h +++ b/variants/lilygo_techo_lite/variant.h @@ -152,8 +152,8 @@ extern const int SCK; // GPS — per LilyGo t_echo_lite_config.h // PIN_GPS_TX/RX named from GPS module's perspective -#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX -#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX +#define PIN_GPS_RX _PINNUM(0, 29) // GPS UART TX → MCU RX +#define PIN_GPS_TX _PINNUM(1, 10) // GPS UART RX ← MCU TX #define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable #define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up -#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS \ No newline at end of file +#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS From 7982ac7fb32b44071fec21ec0f60bb0c815b7c28 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:44:48 +1000 Subject: [PATCH 011/174] nRF52840 Power Management Stage 1 (Boot Lock) - v2.3 preparation work This commit lays some of the ground work for the upcoming power management fix that allows users to configure the boot lock behaviour. It is *not* the full fix yet. The goal is to break up the larger commit into smaller chunks so it is easier to review and softer to merge. Improvements: - MainBoard: Add bool to check if power management has been initialised (later to be consumed by CommonCLI to validate we can safely configure the settings) - MainBoard: Add bool to validate LPCOMP is supported on the board (gates LPCOMP config so it doesnt wedge on an unsupported board) - NRF52Board: Rename initPowerMgr() -> pwrmgtInit() to align with the "pwrmgt" prefix being used by any power management related functions in the upcoming new version - NRF52Board: Add a shutdown reason for "None" so the `get pwrmgt.bootreason` command doesnt erroneously return "Unknown" - NRF52Board: Add gate in configureVoltageWake to not arm LPCOMP when power management isn't initialised or the board doesn't support LPCOMP (i.e. we havent defined the AIN pin) - NRF52Board: Reference the LPCOMP AIN pin directly instead of from the per-board definitions to align to the upcoming deprecation of the per-board static configs - NRF52Board: Drop LPCOMP hysteresis as it broadens the wake voltage too much and makes the board less likely to self-recover - NRF52Board: Separate LPCOMP and VBUS wake arm into their own functions --- src/MeshCore.h | 2 ++ src/helpers/NRF52Board.cpp | 47 ++++++++++++++++++++++---------------- src/helpers/NRF52Board.h | 11 ++++++--- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/MeshCore.h b/src/MeshCore.h index 89e60b1f..70587833 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -69,8 +69,10 @@ public: virtual bool isLoRaFemLnaEnabled() const { return false; } // Power management interface (boards with power management override these) + virtual bool isPwrMgtInitialised() const { return false; } virtual bool isExternalPowered() { return false; } virtual uint16_t getBootVoltage() { return 0; } + virtual bool getWakeLpcompSupported() const { return false; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5fb1e55e..84ab0448 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -38,7 +38,8 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() { g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; } -void NRF52Board::initPowerMgr() { +void NRF52Board::pwrmgtInit() { + if (pwrmgt_initialised) return; // Copy early-captured register values reset_reason = g_nrf52_reset_reason; shutdown_reason = g_nrf52_shutdown_reason; @@ -65,6 +66,7 @@ void NRF52Board::initPowerMgr() { MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)", getResetReasonString(reset_reason), (unsigned long)reset_reason); } + pwrmgt_initialised = true; } const char* NRF52Board::getResetReasonString(uint32_t reason) { @@ -89,6 +91,7 @@ const char* NRF52Board::getResetReasonString(uint32_t reason) { const char* NRF52Board::getShutdownReasonString(uint8_t reason) { switch (reason) { + case SHUTDOWN_REASON_NONE: return "None"; case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage"; case SHUTDOWN_REASON_USER: return "User Request"; case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection"; @@ -97,7 +100,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { } bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { - initPowerMgr(); + pwrmgtInit(); // Read boot voltage boot_voltage_mv = getBattMilliVolts(); @@ -164,24 +167,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) { NVIC_SystemReset(); } -void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { +void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t lpcomp_refsel) { + pwrmgtWakeArmVbus(); + if (!isPwrMgtInitialised()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: Power Management not initialised"); + return; + } + if (!getWakeLpcompSupported()) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: LPCOMP unsupported on variant"); + return; + } // LPCOMP is not managed by SoftDevice - direct register access required // Halt and disable before reconfiguration NRF_LPCOMP->TASKS_STOP = 1; NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled; // Select analog input (AIN0-7 maps to PSEL 0-7) - NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; + NRF_LPCOMP->PSEL = ((uint32_t)PWRMGT_LPCOMP_AIN << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk; // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; + NRF_LPCOMP->REFSEL = ((uint32_t)lpcomp_refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; // Detect UP events (voltage rises above threshold for battery recovery) NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up; - // Enable 50mV hysteresis for noise immunity - NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV; - // Clear stale events/interrupts before enabling wake NRF_LPCOMP->EVENTS_READY = 0; NRF_LPCOMP->EVENTS_DOWN = 0; @@ -195,23 +204,22 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled; NRF_LPCOMP->TASKS_START = 1; - // Wait for comparator to settle before entering SYSTEMOFF + // Wait for comparator to settle for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) { delayMicroseconds(50); } - if (refsel == 7) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel); - } else if (refsel <= 6) { - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)", - ain_channel, refsel + 1); + if (lpcomp_refsel == 7) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN); + } else if (lpcomp_refsel <= 6) { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1); } else { - uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1); - MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)", - ain_channel, ref_num); + uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1); + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num); } +} - // Configure VBUS (USB power) wake alongside LPCOMP +void NRF52Board::pwrmgtWakeArmVbus() { uint8_t sd_enabled = 0; sd_softdevice_is_enabled(&sd_enabled); if (sd_enabled) { @@ -220,8 +228,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_POWER->EVENTS_USBDETECTED = 0; NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk; } - - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed"); } #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f97..0d31726b 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -25,15 +25,15 @@ struct PowerMgtConfig { #endif class NRF52Board : public mesh::MainBoard { -#ifdef NRF52_POWER_MANAGEMENT - void initPowerMgr(); -#endif +private: + bool pwrmgt_initialised = false; protected: uint8_t startup_reason; char *ota_name; #ifdef NRF52_POWER_MANAGEMENT + void pwrmgtInit(); uint32_t reset_reason; // RESETREAS register value uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) @@ -41,6 +41,7 @@ protected: bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void pwrmgtWakeArmVbus(); virtual void initiateShutdown(uint8_t reason); #endif @@ -63,6 +64,10 @@ public: uint8_t getShutdownReason() const override { return shutdown_reason; } const char* getResetReasonString(uint32_t reason) override; const char* getShutdownReasonString(uint8_t reason) override; + bool isPwrMgtInitialised() const override { return pwrmgt_initialised; } + #ifdef PWRMGT_LPCOMP_AIN + bool getWakeLpcompSupported() const override { return true; } + #endif #endif }; From fe72d0321a30397e390c6c21cb6a79ce11de54de Mon Sep 17 00:00:00 2001 From: Bill Plein <260078+bplein@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:51:01 -0500 Subject: [PATCH 012/174] Allow for build-time macro to disable the bluetooth LED Several variants use one or another method to disable the bluetooth LED that is grabbed by the Adafruit Bluefruit library by default. This allows for single build_flags macro to control the behavior (default behavior if not defined, not auto-controlled by Bluefruit if defined). This would open up for future cases where this could be controlled via a CLI command or by an application. Does not change behavior if the flag is not defined. --- src/helpers/nrf52/SerialBLEInterface.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b0..2e5626a8 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -129,6 +129,12 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code char charpin[20]; snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code); + #ifdef DISABLE_BLE_LED \\ use variant platformio.ini build_flags macro: -D DISABLE_BLE_LED + Bluefruit.autoConnLed(false); \\ false removes the automatic control of the LED by the Bluefruit library + #else + Bluefruit.autoConnLed(true); \\ true leaves the default automatic control of the LED by the Bluefruit library + #endif + // If we want to control BLE LED ourselves, uncomment this: // Bluefruit.autoConnLed(false); Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); From 3e828277066f97fbed0e15b67629c95353716116 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 7 Aug 2026 21:47:02 -0400 Subject: [PATCH 013/174] thinknode_m8: initial support --- boards/thinknode_m8.json | 53 +++++++ examples/companion_radio/ui-new/UITask.cpp | 48 ++++++- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 52 ++++++- src/helpers/ui/GxEPDDisplay.h | 10 ++ src/helpers/ui/MomentaryButton.h | 4 +- src/helpers/ui/RotaryInputGPIO.cpp | 34 +++++ src/helpers/ui/RotaryInputGPIO.h | 21 +++ variants/thinknode_m8/ThinkNodeM8Board.cpp | 38 +++++ variants/thinknode_m8/ThinkNodeM8Board.h | 50 +++++++ variants/thinknode_m8/platformio.ini | 160 +++++++++++++++++++++ variants/thinknode_m8/target.cpp | 36 +++++ variants/thinknode_m8/target.h | 30 ++++ variants/thinknode_m8/variant.cpp | 39 +++++ variants/thinknode_m8/variant.h | 141 ++++++++++++++++++ 15 files changed, 705 insertions(+), 12 deletions(-) create mode 100644 boards/thinknode_m8.json create mode 100644 src/helpers/ui/RotaryInputGPIO.cpp create mode 100644 src/helpers/ui/RotaryInputGPIO.h create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.cpp create mode 100644 variants/thinknode_m8/ThinkNodeM8Board.h create mode 100644 variants/thinknode_m8/platformio.ini create mode 100644 variants/thinknode_m8/target.cpp create mode 100644 variants/thinknode_m8/target.h create mode 100644 variants/thinknode_m8/variant.cpp create mode 100644 variants/thinknode_m8/variant.h diff --git a/boards/thinknode_m8.json b/boards/thinknode_m8.json new file mode 100644 index 00000000..acd7f89c --- /dev/null +++ b/boards/thinknode_m8.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"] + ], + "usb_product": "elecrow_thinknode_m8", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M8", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "elecrow thinknode m8", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "", + "vendor": "ELECROW" +} diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31..d5e95d15 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,10 +2,15 @@ #include #include "../MyMesh.h" #include "target.h" +#include #ifdef WIFI_SSID #include #endif +#ifndef UI_TZ_OFFSET + #define UI_TZ_OFFSET 0 +#endif + #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -23,7 +28,7 @@ #define UI_RECENT_LIST_SIZE 4 #endif -#if UI_HAS_JOYSTICK +#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT #define PRESS_LABEL "press Enter" #else #define PRESS_LABEL "long press" @@ -224,7 +229,19 @@ public: display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.drawTextCentered(display.width() / 2, 22, tmp); - + + #ifdef UI_SHOW_CLOCK + display.setTextSize(3); + uint32_t now = _rtc->getCurrentTime(); + int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... + now += (int32_t)tz * 3600; + DateTime dt (now); + sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); + display.drawTextCentered(display.width() / 2, 60, tmp); + display.setTextSize(1); + sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); + display.drawTextCentered(display.width() / 2, 80, tmp); + #endif #ifdef WIFI_SSID IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); @@ -234,13 +251,21 @@ public: if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); + #ifdef UI_SHOW_CLOCK + display.drawTextCentered(display.width() / 2, 110, "< Connected >"); + #else display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - + #endif } else if (the_mesh.getBLEPin() != 0) { // BT pin display.setColor(UIColor::warning_txt); - display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); + #ifdef UI_SHOW_CLOCK + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 110, tmp); + #else + display.setTextSize(2); display.drawTextCentered(display.width() / 2, 43, tmp); + #endif } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); @@ -720,6 +745,9 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { + display.forceFullRefresh(); + display.clear(); + display.endFrame(); // Power off board including radio, display, GPS and components _board->powerOff(); } @@ -760,6 +788,17 @@ void UITask::loop() { } #elif defined(PIN_USER_BTN) int ev = user_btn.check(); + #ifdef UI_HAS_NAV_INPUT + if (ev == BUTTON_EVENT_CLICK) { + c = checkDisplayOn(KEY_ENTER); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + display.turnOff(); + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + c = handleDoubleClick(KEY_SELECT); + } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { + c = handleTripleClick(KEY_SELECT); + } + #else if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { @@ -769,6 +808,7 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } + #endif #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index b76a1b6c..3e9e2dde 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -23,6 +23,7 @@ public: virtual bool isOn() = 0; virtual bool isEink() { return false; } // default to non-eink, override in eink drivers + virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 13dafa34..11f78b57 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,10 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +#ifndef EPD_WASHING_MACHINE_CYCLES + #define EPD_WASHING_MACHINE_CYCLES 0 +#endif + // Color scheme ColorVal UIColor::window_bkg = GxEPD_WHITE; ColorVal UIColor::title_bkg = GxEPD_WHITE; @@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE; ColorVal UIColor::popup_txt = GxEPD_BLACK; ColorVal UIColor::corp_blue = GxEPD_BLACK; - bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() { display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); setTextSize(1); // Default to size 1 - display.setPartialWindow(0, 0, display.width(), display.height()); - display.fillScreen(GxEPD_WHITE); - display.display(true); + display.setFullWindow(); + + for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) { + display.fillScreen(GxEPD_BLACK); + display.display(false); + delay(2000); + display.fillScreen(GxEPD_WHITE); + display.display(false); + delay(2000); + } + + display.setPartialWindow(0, 0, display.width(), display.height()); + resetPartialRefreshCounter(); + #if DISP_BACKLIGHT digitalWrite(DISP_BACKLIGHT, LOW); pinMode(DISP_BACKLIGHT, OUTPUT); #endif _init = true; + _isOn = true; return true; } @@ -55,7 +70,11 @@ void GxEPDDisplay::turnOn() { #elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif - _isOn = true; + if (!_isOn) { + forceFullRefresh(); + _isOn = true; + last_display_crc_value=0; + } } void GxEPDDisplay::turnOff() { @@ -65,6 +84,11 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; + display.setFullWindow(); + display.fillScreen(GxEPD_WHITE); + display.display(true); + forceFullRefresh(); + display.powerOff(); } void GxEPDDisplay::clear() { @@ -77,6 +101,12 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); + if (_cycles_before_full_refresh <= 0) { + display.setPartialWindow(0, 0, display.width(), display.height()); + } else { + display.setFullWindow(); + display.writeScreenBuffer(); + } } void GxEPDDisplay::setTextSize(int sz) { @@ -178,9 +208,19 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) { } void GxEPDDisplay::endFrame() { + if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - display.display(true); + if (_cycles_before_full_refresh == 0) { + display.display(false); + display.writeScreenBuffer(); + resetPartialRefreshCounter(); + } else { + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; + } + } last_display_crc_value = crc; } } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c653eac4..8a37b72c 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,6 +16,12 @@ #include "DisplayDriver.h" +#ifndef EINK_MAX_PARTIAL_REFRESH + // 60 to prevent ghosting when refreshing every minute ... + // set to -1 to disable the counter + #define EINK_MAX_PARTIAL_REFRESH -1 +#endif + class GxEPDDisplay : public DisplayDriver { #if defined(EINK_DISPLAY_MODEL) @@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver { uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; + int _cycles_before_full_refresh; + int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after public: #if defined(EINK_DISPLAY_MODEL) @@ -48,6 +56,8 @@ public: bool isOn() override { return _isOn; } bool isEink() override { return true; } + void forceFullRefresh() override { _cycles_before_full_refresh = 0; }; + void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; void turnOn() override; void turnOff() override; void clear() override; diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h index 358a343b..351545ce 100644 --- a/src/helpers/ui/MomentaryButton.h +++ b/src/helpers/ui/MomentaryButton.h @@ -25,8 +25,8 @@ class MomentaryButton { public: MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true); MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold); - void begin(); - int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* + virtual void begin(); + virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state) uint8_t getPin() { return _pin; } bool isPressed() const; diff --git a/src/helpers/ui/RotaryInputGPIO.cpp b/src/helpers/ui/RotaryInputGPIO.cpp new file mode 100644 index 00000000..b094fe49 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.cpp @@ -0,0 +1,34 @@ +#include "RotaryInputGPIO.h" + +bool RotaryInputGPIO::begin() { + + if (_pin_a >= 0) { + pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT); + } + if (_pin_b >= 0) { + pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT); + } + + b_prec = digitalRead(_pin_b); + + return true; +} + +RotaryInputEvent RotaryInputGPIO::poll() { + RotaryInputEvent ev = RotaryInputEvent::None; + bool a = digitalRead(_pin_a); + bool b = digitalRead(_pin_b); + + // this is the simplest scheme and it works well + // for thinknode M8, just read A when B rises ;) + if (!b_prec && b) { // rising edge of A + if (a) { + ev = RotaryInputEvent::Next; + } else { + ev = RotaryInputEvent::Prev; + } + } + b_prec = b; + + return ev; +} \ No newline at end of file diff --git a/src/helpers/ui/RotaryInputGPIO.h b/src/helpers/ui/RotaryInputGPIO.h new file mode 100644 index 00000000..16b7e46a --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "RotaryInput.h" + +#define BUTTON_EVENT_UP 5 +#define BUTTON_EVENT_DOWN 6 + +class RotaryInputGPIO: public RotaryInput { + int8_t _pin_a; + bool _pull_a; + int8_t _pin_b; + bool _pull_b; + bool b_prec; +public: + RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {} + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return true;} +}; diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp new file mode 100644 index 00000000..2ca393d9 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -0,0 +1,38 @@ +#include +#include + +#include "ThinkNodeM8Board.h" + +#ifdef THINKNODE_M8 + +void ThinkNodeM8Board::begin() { + NRF52Board::begin(); + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM8Board::getBattMilliVolts() { + int adcvalue = 0; + + digitalWrite(ADC_EN, HIGH); + analogReference(AR_INTERNAL_2_4); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + digitalWrite(ADC_EN, LOW); + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} +#endif diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h new file mode 100644 index 00000000..73aeb1f5 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +// built-ins +#define VBAT_MV_PER_LSB (0.5859375F) // 2.4V ADC range and 12-bit ADC resolution = 2400mV/4096 + +#define VBAT_DIVIDER (0.57F) // 150K + 150K voltage divider on VBAT +#define VBAT_DIVIDER_COMP (1.75F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (4) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class ThinkNodeM8Board : public NRF52Board { +public: + ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {} + void begin(); + uint16_t getBattMilliVolts() override; + + #if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } + #endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode-M8"; + } + + void shutdownPeripherals() override { + // power off board + NRF52Board::shutdownPeripherals(); + + // make sure every gate is closed + digitalWrite(DISP_EN, LOW); + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(SX126X_ANT_SW, LOW); + digitalWrite(ADC_EN, LOW); + + #ifdef PIN_BUTTON1 // Use BTN to go out of sleep + nrf_gpio_cfg_sense_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + #endif + } +}; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini new file mode 100644 index 00000000..9f1595c5 --- /dev/null +++ b/variants/thinknode_m8/platformio.ini @@ -0,0 +1,160 @@ +[ThinkNode_M8] +extends = nrf52_base +board = thinknode_m8 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m8 + -D THINKNODE_M8=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=25 + -D P_LORA_DIO_2=32 + -D P_LORA_NSS=21 + -D P_LORA_RESET=24 + -D P_LORA_BUSY=32 # DIO2 + -D P_LORA_SCLK=19 + -D P_LORA_MISO=22 + -D P_LORA_MOSI=20 + -D UI_SHOW_CLOCK=1 + -D UI_HAS_ROTARY_INPUT=1 + -D UI_HAS_NAV_INPUT=1 + -D UI_RECENT_LIST_SIZE=9 + -D UI_TZ_OFFSET=-4 # GMT-4 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 +; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 + -D DISABLE_DIAGNOSTIC_OUTPUT + -D SX126X_POWER_EN=37 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/thinknode_m8> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 +debug_tool = jlink +upload_protocol = nrfutil + +[env:ThinkNode_M8_repeater] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_room_server] +extends = ThinkNode_M8 +build_flags = + ${ThinkNode_M8.build_flags} + -D ADVERT_NAME='"ThinkNode Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/simple_room_server/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + +[env:ThinkNode_M8_companion_radio_ble] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 +# -D BLE_DEBUG_LOGGING=1 + -D DISPLAY_ROTATION=4 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=33 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D QSPIFLASH=1 + -D ENV_INCLUDE_GPS=1 +; -D GPS_NMEA_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.9 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_companion_radio_usb] +extends = ThinkNode_M8 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${ThinkNode_M8.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_ROTATION=4 + -D QSPIFLASH=1 + -D DISPLAY_CLASS=GxEPDDisplay + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=0 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=6 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M8.build_src_filter} + + + + + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${ThinkNode_M8.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.2 + bakercp/CRC32 @ ^2.0.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M8_kiss_modem] +extends = ThinkNode_M8 +build_src_filter = ${ThinkNode_M8.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp new file mode 100644 index 00000000..ac4ac100 --- /dev/null +++ b/variants/thinknode_m8/target.cpp @@ -0,0 +1,36 @@ +#include +#include "target.h" +#include +#include +#include + +ThinkNodeM8Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h new file mode 100644 index 00000000..c3402a01 --- /dev/null +++ b/variants/thinknode_m8/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include + #include +#endif + +extern ThinkNodeM8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern RotaryInputGPIO rotary_input; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m8/variant.cpp b/variants/thinknode_m8/variant.cpp new file mode 100644 index 00000000..e15c0842 --- /dev/null +++ b/variants/thinknode_m8/variant.cpp @@ -0,0 +1,39 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const int MISO = PIN_SPI1_MISO; +const int MOSI = PIN_SPI1_MOSI; +const int SCK = PIN_SPI1_SCK; + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + pinMode(DISP_EN, OUTPUT); + digitalWrite(DISP_EN, HIGH); + + pinMode(PIN_BUTTON1, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_A, INPUT_PULLDOWN); + pinMode(PIN_BUTTON1_B, INPUT_PULLDOWN); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + + // shutdown gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // disable at startup + + pinMode(SX126X_ANT_SW, OUTPUT); // ANT_SW + digitalWrite(SX126X_ANT_SW, HIGH); + + pinMode(ADC_EN, OUTPUT); + digitalWrite(ADC_EN, LOW); + +} diff --git a/variants/thinknode_m8/variant.h b/variants/thinknode_m8/variant.h new file mode 100644 index 00000000..9c36c772 --- /dev/null +++ b/variants/thinknode_m8/variant.h @@ -0,0 +1,141 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_TXCO (21) +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_PWR_EN (13) // I2C + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (1.75F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (2.4) + +#define ADC_EN (40) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (22) +#define PIN_SPI_MOSI (20) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (21) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs +#define LED_BLUE (-1) + +#define LED_BUILTIN LED_BLUE +#define LED_PIN LED_BUILTIN +#define LED_STATE_ON HIGH + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (6) +#define PIN_BUTTON1_A (8) // Rotary button/Encoder +#define PIN_BUTTON1_B (41) +#define BUTTON_PIN PIN_BUTTON1 + +#define PIN_BUTTON2 (12) +#define BUTTON_PIN2 PIN_BUTTON2 + +#define PIN_USER_BTN PIN_BUTTON1 + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_ANT_SW (23) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +//////////////////////////////////////////////////////////////////////////////// +// SPI1 +#define PIN_SPI1_NSS (30) +#define PIN_SPI1_SCK (31) +#define PIN_SPI1_MOSI (29) +#define PIN_SPI1_MISO (-1) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +//////////////////////////////////////////////////////////////////////////////// +// QSPI +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI +#define PIN_QSPI_SCK (46) +#define PIN_QSPI_CS (47) +#define PIN_QSPI_IO0 (44) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (45) // MISO if using two bit interface +#define PIN_QSPI_IO2 (7) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (5) // HOLD if using two bit interface (i.e. not used) + +//////////////////////////////////////////////////////////////////////////////// +// Display + +#define DISP_MISO PIN_SPI1_MISO +#define DISP_MOSI PIN_SPI1_MOSI +#define DISP_SCLK PIN_SPI1_SCK +#define PIN_DISPLAY_CS PIN_SPI1_NSS +#define PIN_DISPLAY_DC (28) +#define PIN_DISPLAY_RST (2) +#define PIN_DISPLAY_BUSY (3) +#define DISP_BACKLIGHT (43) +#define DISP_EN (42) + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define PIN_GPS_RX (36) +#define PIN_GPS_TX (34) +#define PIN_GPS_EN (16) +#define PIN_GPS_RESET (-1) +#define PIN_GPS_PPS (14) +#define PIN_GPS_STANDBY (15) // Low = sleep +#define GPS_BAUD_RATE 115200 From fc26cae198cbfb6028bc9c53c52e93bd2afecd07 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:02 +0200 Subject: [PATCH 014/174] Update LocationProvider.h --- src/helpers/sensors/LocationProvider.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652..a70ae8ca 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -1,6 +1,8 @@ #pragma once #include "Mesh.h" +#include +#include class LocationProvider { @@ -22,4 +24,14 @@ public: virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + + // Format compact diagnostics in the caller-provided buffer. Providers that + // have more information (for example UART counters) can override this. + virtual void formatDiagnostics(char* out, size_t out_size) { + if (out_size == 0) return; + snprintf(out, out_size, "en:%u sat:%ld fix:%u", + isEnabled() ? 1U : 0U, + satellitesCount(), + isValid() ? 1U : 0U); + } }; From dcd601135e9a4439bcb647c29efaae023ba8ac23 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:31 +0200 Subject: [PATCH 015/174] Update MicroNMEALocationProvider.h --- .../sensors/MicroNMEALocationProvider.h | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb114..0a6ebc1d 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef GPS_EN #ifdef PIN_GPS_EN @@ -51,6 +52,32 @@ class MicroNMEALocationProvider : public LocationProvider { unsigned long _last_time_sync = 0; static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes + // Observation-only diagnostics. These fields do not change GPS power, + // timing, reset behaviour, or parser input. + uint32_t _uart_bytes = 0; + uint32_t _nmea_checksum_ok = 0; + uint32_t _nmea_checksum_bad = 0; + uint32_t _begin_calls = 0; + uint32_t _stop_calls = 0; + unsigned long _last_uart_ms = 0; + unsigned long _last_nmea_ms = 0; + unsigned long _last_fix_ms = 0; + bool _uart_seen = false; + bool _nmea_seen = false; + bool _fix_seen = false; + + static uint32_t ageMs(unsigned long timestamp) { + return (uint32_t)(millis() - timestamp); + } + + static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) { + if (!seen) { + snprintf(out, out_size, "never"); + } else { + snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp)); + } + } + public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) { @@ -76,6 +103,7 @@ public : } void begin() override { + _begin_calls++; claim(); if (_pin_en != -1) { digitalWrite(_pin_en, GPS_EN_ACTIVE); @@ -94,6 +122,7 @@ public : } void stop() override { + _stop_calls++; if (_pin_en != -1) { digitalWrite(_pin_en, !GPS_EN_ACTIVE); } @@ -137,10 +166,36 @@ public : while (_gps_serial->available()) { char c = _gps_serial->read(); + _uart_bytes++; + _last_uart_ms = millis(); + _uart_seen = true; #ifdef GPS_NMEA_DEBUG Serial.print(c); #endif - nmea.process(c); + bool parsed = nmea.process(c); + + // MicroNMEA leaves the completed sentence in its buffer after the + // first line terminator. CRLF therefore counts once: CR observes the + // sentence, then LF clears the now-empty buffer. + if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') { + if (MicroNMEA::testChecksum(nmea.getSentence())) { + _nmea_checksum_ok++; + _last_nmea_ms = millis(); + _nmea_seen = true; + + // Refresh fix age only when a newly parsed GGA/RMC sentence + // carries a valid fix. Other valid NMEA sentences must not + // make an old position look fresh. + const char* message_id = nmea.getMessageID(); + if (parsed && isValid() && + (strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) { + _last_fix_ms = millis(); + _fix_seen = true; + } + } else { + _nmea_checksum_bad++; + } + } } if (!isValid()) time_valid = 0; @@ -163,4 +218,31 @@ public : } } } + + void formatDiagnostics(char* out, size_t out_size) override { + if (out_size == 0) return; + + char uart_age[11]; + char nmea_age[11]; + char fix_age[11]; + formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms); + formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms); + formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms); + + // Compact enough for CommonCLI's 160-byte reply buffer, including the + // "req:" prefix added by CommonCLI. + snprintf(out, out_size, + "en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu", + isEnabled() ? 1U : 0U, + (unsigned long)_uart_bytes, + uart_age, + (unsigned long)_nmea_checksum_ok, + (unsigned long)_nmea_checksum_bad, + nmea_age, + satellitesCount(), + isValid() ? 1U : 0U, + fix_age, + (unsigned long)_begin_calls, + (unsigned long)_stop_calls); + } }; From f110ce735c639425e1eef8c5f65906e0d44a03d2 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Thu, 13 Aug 2026 23:52:58 +0200 Subject: [PATCH 016/174] Update CommonCLI.cpp --- src/helpers/CommonCLI.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..b8fd6d56 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -344,6 +344,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "gps provider not found"); } + } else if (strcmp(command, "gps diag") == 0) { + LocationProvider * l = _sensors->getLocationProvider(); + if (l != NULL) { + const char* setting = _sensors->getSettingByKey("gps"); + bool requested = setting != NULL && strcmp(setting, "1") == 0; + int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U); + if (prefix_len < 0 || prefix_len >= 160) { + reply[159] = 0; + } else { + l->formatDiagnostics(reply + prefix_len, 160 - prefix_len); + } + } else { + strcpy(reply, "gps provider not found"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; From 0a2801649610cc8d8053cc26a063832ef4c31232 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:20:11 +0200 Subject: [PATCH 017/174] Update MicroNMEALocationProvider.h --- src/helpers/sensors/MicroNMEALocationProvider.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb114..a8bdbe0b 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -51,6 +51,17 @@ class MicroNMEALocationProvider : public LocationProvider { unsigned long _last_time_sync = 0; static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes +#ifdef GPS_L76K + void configureL76K() { + // Reapply the constellation mode at each provider start instead of + // assuming it survives a receiver power cycle. + // Mode 7 enables GPS + BeiDou + GLONASS. + delay(250); + MicroNMEA::sendSentence(*_gps_serial, "$PCAS04,7"); + delay(250); + } +#endif + public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) { @@ -91,6 +102,11 @@ public : delay(10); digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } +#ifdef GPS_L76K + // EnvironmentSensorManager calls reset() immediately after begin(). + // Configure only after that reset so the setting is not discarded. + configureL76K(); +#endif } void stop() override { From 3bcfb70141983b7b94b64108cac1cc6092b1a04c Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:42:57 +0200 Subject: [PATCH 018/174] Update MeshCore.h --- src/MeshCore.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef..22979476 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -74,6 +74,8 @@ public: // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } + // True while a board can identify an active battery-charging source. + virtual bool isChargerActive() { return false; } virtual uint16_t getBootVoltage() { return 0; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } From 77057aefdd392c09c744497f6103c3003f30329f Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:43:33 +0200 Subject: [PATCH 019/174] Update CommonCLI.cpp --- src/helpers/CommonCLI.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..d2a8b707 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -273,6 +273,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { sprintf(reply, "%s", _board->getManufacturerName()); + } else if (strcmp(command, "power") == 0) { + snprintf(reply, 160, "batt:%u mV ext:%s charger:%s", + (unsigned)_board->getBattMilliVolts(), + _board->isExternalPowered() ? "yes" : "no", + _board->isChargerActive() ? "yes" : "no"); } else if (memcmp(command, "sensor get ", 11) == 0) { const char* key = command + 11; const char* val = _sensors->getSettingByKey(key); From 4c4497d077da23304c53890dcdee31872fb55268 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:44:16 +0200 Subject: [PATCH 020/174] Update ThinkNodeM6Board.cpp --- variants/thinknode_m6/ThinkNodeM6Board.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 8ebae64c..f83f274f 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -15,6 +15,9 @@ void ThinkNodeM6Board::begin() { digitalWrite(P_LORA_TX_LED, LOW); #endif + pinMode(EXT_PWR_DETECT, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT); + delay(10); // give sx1262 some time to power up } From 33239520e13a384149ab920a76995175d836738d Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:44:43 +0200 Subject: [PATCH 021/174] Update ThinkNodeM6Board.h --- variants/thinknode_m6/ThinkNodeM6Board.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 78815e2c..1af952d2 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -36,6 +36,21 @@ public: return "Elecrow ThinkNode M6"; } + bool isExternalPowered() override { + if (digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE || + digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE) { + return true; + } + return NRF52Board::isExternalPowered(); + } + + bool isChargerActive() override { + // The charge-status line covers solar input; external USB input is also a + // charging source, although the board cannot report charge completion. + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || + digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE; + } + void powerOff() override { // turn off all leds, sd_power_system_off will not do this for us From bb7a324471f4070966530bc3c6b7b6680164119a Mon Sep 17 00:00:00 2001 From: F4FPR Date: Fri, 14 Aug 2026 00:45:22 +0200 Subject: [PATCH 022/174] Update variant.h --- variants/thinknode_m6/variant.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/variants/thinknode_m6/variant.h b/variants/thinknode_m6/variant.h index 0a4025f3..3428fcd6 100644 --- a/variants/thinknode_m6/variant.h +++ b/variants/thinknode_m6/variant.h @@ -29,6 +29,12 @@ #define AREF_VOLTAGE (3.0) +// External power and charger status lines. +#define EXT_PWR_DETECT (13) +#define EXT_PWR_DETECT_VALUE HIGH +#define EXT_CHRG_DETECT (15) +#define EXT_CHRG_DETECT_VALUE LOW + //////////////////////////////////////////////////////////////////////////////// // Number of pins From 114093ee0e8fd34ca7d7434e8cb8523bd6e1ba9d Mon Sep 17 00:00:00 2001 From: Fedor Kallay Date: Sat, 15 Aug 2026 02:13:11 +0200 Subject: [PATCH 023/174] fix(lr2021): setTxPower() could leave the receiver down until reboot RadioLibWrapper::setTxPower() called setOutputPower() and nothing else. On LR2021 that writes the PA config and TxParams, which are standby-only commands, and it never re-arms the receiver. This only affects LR2021 boards. recvRaw() has: #if defined(USE_LR2021) state = STATE_RX; // LR2021 stays in Rx after readData #else state = STATE_IDLE; // need another startReceive() #endif so on SX126x the next recvRaw() re-arms Rx anyway and the write is harmless, while on LR2021 startReceive() is never called again on its own. A 'set tx' issued while the radio was listening could therefore stop reception until the next reboot. Observed a few times on an LR2021 repeater; recovery required a power cycle. Dispatcher's stuck-radio check does not catch it: isInRecvMode() reports the wrapper's own `state` flag rather than the chip's actual mode, so it still believes the radio is in Rx, and it only raises ERR_EVENT_STARTRX_TIMEOUT without attempting recovery. Fix: on LR2021, drop to standby via idle() before writing the PA config and let checkRecv() re-arm Rx - the same pattern resetAGC() and applySideDetectorConfig() already use. Other radios are left untouched. Affects meshtracker_x1 and meshnology_w12 (both USE_LR2021). Tested on hardware with an LR2021 repeater: six consecutive 'set tx' changes (15/18/21/14/19/14), after which the radio kept receiving and forwarding traffic (rawrx/rxpkts increasing, no missed IRQs). Noise-floor sampling also kept updating, which only happens while state == STATE_RX. --- src/helpers/radiolib/RadioLibWrappers.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b9c095ac..f404b625 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -48,6 +48,17 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { +#if defined(USE_LR2021) + // On LR2021, setOutputPower() writes the PA config and TxParams, which are + // standby-only commands. recvRaw() keeps state == STATE_RX after readData on + // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which + // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls + // startReceive() again by itself. Writing the PA config while listening could + // therefore leave the receiver down until the next reboot. Drop to standby + // first and let checkRecv() re-arm Rx, as resetAGC() and + // applySideDetectorConfig() already do. + idle(); +#endif _radio->setOutputPower(dbm); } From 0a595ae79db209add0c1addffbae2626ea725cb7 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 16 Aug 2026 11:40:13 -0400 Subject: [PATCH 024/174] gxepddisplay: better handling of full refresh (no faint partial afterwards !!!) --- src/helpers/ui/GxEPDDisplay.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 11f78b57..610b8541 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -88,7 +88,7 @@ void GxEPDDisplay::turnOff() { display.fillScreen(GxEPD_WHITE); display.display(true); forceFullRefresh(); - display.powerOff(); + display.hibernate(); } void GxEPDDisplay::clear() { @@ -101,11 +101,15 @@ void GxEPDDisplay::startFrame(ColorVal bkg) { display.fillScreen(bkg); display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); - if (_cycles_before_full_refresh <= 0) { + if (_cycles_before_full_refresh != 0) { display.setPartialWindow(0, 0, display.width(), display.height()); } else { - display.setFullWindow(); - display.writeScreenBuffer(); + // forces a full wipe of the screen ... + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + // we'll need a partial refresh after that (whatever crc value is) + last_display_crc_value = 0; + resetPartialRefreshCounter(); } } @@ -211,16 +215,10 @@ void GxEPDDisplay::endFrame() { if (_isOn == false) return; uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { - if (_cycles_before_full_refresh == 0) { - display.display(false); - display.writeScreenBuffer(); - resetPartialRefreshCounter(); - } else { - display.display(true); - if (_cycles_before_full_refresh > 0) { - _cycles_before_full_refresh--; - } + display.display(true); + if (_cycles_before_full_refresh > 0) { + _cycles_before_full_refresh--; } - last_display_crc_value = crc; } + last_display_crc_value = crc; } From a8a60015769c712b3af25cd6c671cb0c9c706698 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 025/174] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb9..cac6c4a2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f706..e0654464 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 9387f55d7e3637becce6e30d6472be3a06da145f Mon Sep 17 00:00:00 2001 From: Huw Duddy <37787853+oltaco@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:16:01 +1000 Subject: [PATCH 026/174] trimmed comment --- src/helpers/radiolib/RadioLibWrappers.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index f404b625..e4d2ba1c 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -49,14 +49,6 @@ uint32_t RadioLibWrapper::getRngSeed() { void RadioLibWrapper::setTxPower(int8_t dbm) { #if defined(USE_LR2021) - // On LR2021, setOutputPower() writes the PA config and TxParams, which are - // standby-only commands. recvRaw() keeps state == STATE_RX after readData on - // this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which - // falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls - // startReceive() again by itself. Writing the PA config while listening could - // therefore leave the receiver down until the next reboot. Drop to standby - // first and let checkRecv() re-arm Rx, as resetAGC() and - // applySideDetectorConfig() already do. idle(); #endif _radio->setOutputPower(dbm); @@ -265,4 +257,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +} From de0d5031decd44f1d8659d58db89e6c823408755 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:24:17 -0400 Subject: [PATCH 027/174] M8: preview messages up to 164 cars --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- variants/thinknode_m8/platformio.ini | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d5e95d15..59b58461 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -511,6 +511,10 @@ public: } }; +#ifndef UI_MSG_PREVIEW_SIZE + #define UI_MSG_PREVIEW_SIZE 78 +#endif + class MsgPreviewScreen : public UIScreen { UITask* _task; mesh::RTCClock* _rtc; @@ -518,7 +522,7 @@ class MsgPreviewScreen : public UIScreen { struct MsgEntry { uint32_t timestamp; char origin[62]; - char msg[78]; + char msg[UI_MSG_PREVIEW_SIZE]; }; #define MAX_UNREAD_MSGS 32 int num_unread; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 9f1595c5..b8c70973 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -23,6 +23,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 -D UI_TZ_OFFSET=-4 # GMT-4 + -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 From 7f664f100dad6e99657ef409fe3f3c873a507316 Mon Sep 17 00:00:00 2001 From: Florent Date: Mon, 17 Aug 2026 09:41:51 -0400 Subject: [PATCH 028/174] gxepd: do full refresh before screen off ... so wakes quicker ;) --- src/helpers/ui/GxEPDDisplay.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 610b8541..332157eb 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -71,9 +71,7 @@ void GxEPDDisplay::turnOn() { expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH); #endif if (!_isOn) { - forceFullRefresh(); _isOn = true; - last_display_crc_value=0; } } @@ -84,10 +82,12 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; - display.setFullWindow(); - display.fillScreen(GxEPD_WHITE); - display.display(true); - forceFullRefresh(); + // do full refresh before powering off to clear screen + // no full refresh needed at wakeup + display.clearScreen(0xFF); // Clears microcontroller side RAM + display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers + resetPartialRefreshCounter(); + last_display_crc_value=0; display.hibernate(); } From 2b0ed0000494a228536328fa19ddbcb4e774e26e Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:34:19 -0700 Subject: [PATCH 029/174] fix(sensors): claim an I2C address after a successful init Several table entries share an address and not every driver verifies a chip ID: INA226::begin() only checks that the address ACKs, so an SHT4x at 0x44 was also registered as an INA226 and reported junk current on a second channel. Mark the address consumed once a driver initializes it so later entries cannot re-claim the same device. --- src/helpers/sensors/EnvironmentSensorManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index e2f0d33e..c3dfd9f7 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -650,6 +650,7 @@ bool EnvironmentSensorManager::begin() { continue; } MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address); + detected[def.address] = false; // consumed; later entries must not re-claim this device for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) { _active_sensors[_active_sensor_count++] = { def.query, sub }; } From 2a6eabe12012f06a7f1a9ae06c25c50161e67ecf Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 17 Aug 2026 14:59:59 -0700 Subject: [PATCH 030/174] fix(sensors): probe BMP/BME at both 0x76 and 0x77 Grove and other Bosch modules strap SDO high, so the 0x76-only table never initialized them. Add an alternate-address entry per Bosch sensor; the bus scan still gates every probe, and all four drivers verify a chip ID before claiming an address. Each sensor type has a single static driver instance, so skip an entry whose query is already active: the alternate address is a fallback, not a second device. --- .../sensors/EnvironmentSensorManager.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index c3dfd9f7..54305692 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -539,6 +539,8 @@ static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) { // are compiled in. The sentinel at the end keeps the array // non-empty regardless of which sensors are enabled. // +// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both. +// // Ordering here determines channel assignment at runtime: // the first detected+initialized sensor gets channel 2, the // next gets channel 3, and so on. @@ -551,21 +553,27 @@ struct SensorDef { void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry); }; +#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76)) + static const SensorDef SENSOR_TABLE[] = { #if ENV_INCLUDE_AHTX0 { TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 }, #endif #ifdef ENV_INCLUDE_BME680 - { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 }, #endif #if ENV_INCLUDE_BME680_BSEC - { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, #endif #if ENV_INCLUDE_BME280 - { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 }, #endif #if ENV_INCLUDE_BMP280 - { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + { TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 }, #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, @@ -603,6 +611,8 @@ static const SensorDef SENSOR_TABLE[] = { { 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty }; +#undef TELEM_BOSCH_ALT_ADDR + static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1; // ============================================================ @@ -640,6 +650,12 @@ bool EnvironmentSensorManager::begin() { _active_sensor_count = 0; for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) { const SensorDef& def = SENSOR_TABLE[i]; + // One static driver instance per type: an alternate address is a fallback, not a second device. + bool already_active = false; + for (int j = 0; j < _active_sensor_count; j++) { + if (_active_sensors[j].query == def.query) { already_active = true; break; } + } + if (already_active) continue; if (!detected[def.address]) { MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address); continue; From 0cce9197a18572d484a284058a896c8d1a7cc0bb Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 18 Aug 2026 13:11:15 +1000 Subject: [PATCH 031/174] Fix for CMD_SEND_RAW_DATA and multi-byte paths --- examples/companion_radio/MyMesh.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c334066..16603bc4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1512,15 +1512,19 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); + if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { + uint8_t path[MAX_PATH_SIZE]; + i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); + if (i + 4 > len) { // min payload 4 bytes + writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else { - writeErrFrame(ERR_CODE_TABLE_FULL); + auto pkt = createRawData(&cmd_frame[i], len - i); + if (pkt) { + sendDirect(pkt, path, path_len); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) From 38f10cd12b0c766db9599973b2fc751da29af908 Mon Sep 17 00:00:00 2001 From: Dominik Tyrala Date: Mon, 17 Aug 2026 15:50:46 +0200 Subject: [PATCH 032/174] Fixes BLE exceeding flash size --- variants/xiao_c3/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 587c5c27..c9c107c6 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -73,6 +73,7 @@ lib_deps = [env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + From d65da5a81a5385a10d99212ad1be0f8417f0b593 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 13:51:55 +0200 Subject: [PATCH 033/174] fix: move MeshadventurerBoard.h to correct location --- {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h | 2 +- variants/meshadventurer/target.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename {src/helpers => variants/meshadventurer}/MeshadventurerBoard.h (97%) diff --git a/src/helpers/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h similarity index 97% rename from src/helpers/MeshadventurerBoard.h rename to variants/meshadventurer/MeshadventurerBoard.h index 0325161d..d9e88e1b 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -13,7 +13,7 @@ #define PIN_VBAT_READ 35 -#include "ESP32Board.h" +#include "helpers/ESP32Board.h" class MeshadventurerBoard : public ESP32Board { diff --git a/variants/meshadventurer/target.h b/variants/meshadventurer/target.h index d59b3aee..0749565d 100644 --- a/variants/meshadventurer/target.h +++ b/variants/meshadventurer/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -44,4 +44,3 @@ extern MASensorManager sensors; bool radio_init(); mesh::LocalIdentity radio_new_identity(); - From 2db4b40617c3d285342d7f418be254ebbfead75b Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 18 Aug 2026 14:56:27 +0200 Subject: [PATCH 034/174] repeater: set `loop.detect` to `minimal` as default for new installs --- examples/simple_repeater/MyMesh.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3..a711ec0a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -341,7 +341,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t int results_offset = 0; uint8_t results_buffer[130]; for(int index = 0; index < count && index + offset < neighbours_count; index++){ - + // stop if we can't fit another entry in results int entry_size = pubkey_prefix_length + 4 + 1; if(results_offset + entry_size > sizeof(results_buffer)){ @@ -907,6 +907,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.loop_detect = LOOP_DETECT_MINIMAL; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -1170,7 +1171,7 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } From da863e555d811155c97bd3cda5517b42d9096bda Mon Sep 17 00:00:00 2001 From: F4FPR Date: Tue, 18 Aug 2026 19:38:23 +0200 Subject: [PATCH 035/174] Update MeshCore.h --- src/MeshCore.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index 22979476..ca12daee 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -76,6 +76,14 @@ public: virtual bool isExternalPowered() { return false; } // True while a board can identify an active battery-charging source. virtual bool isChargerActive() { return false; } + + // Optional, source-specific power detection. Boards that can distinguish a USB + // supply from a solar charger override these; defaults keep every other board + // unaffected (the generic CLI prints "n/a" when a capability is absent). + virtual bool hasUsbPowerDetect() const { return false; } + virtual bool isUsbPowered() { return false; } + virtual bool hasSolarChargerDetect() const { return false; } + virtual bool isSolarChargerActive() { return false; } virtual uint16_t getBootVoltage() { return 0; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } From 6b696a4ef6db2e757f300b1d55efa34705f4e142 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Tue, 18 Aug 2026 19:39:21 +0200 Subject: [PATCH 036/174] Update CommonCLI.cpp --- src/helpers/CommonCLI.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index d2a8b707..f2daf3cd 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -274,8 +274,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "board", 5) == 0) { sprintf(reply, "%s", _board->getManufacturerName()); } else if (strcmp(command, "power") == 0) { - snprintf(reply, 160, "batt:%u mV ext:%s charger:%s", + const char* usb_state = _board->hasUsbPowerDetect() + ? (_board->isUsbPowered() ? "yes" : "no") + : "n/a"; + const char* solar_state = _board->hasSolarChargerDetect() + ? (_board->isSolarChargerActive() ? "yes" : "no") + : "n/a"; + snprintf(reply, 160, "batt:%u mV usb:%s solar_chg:%s ext:%s charger:%s", (unsigned)_board->getBattMilliVolts(), + usb_state, + solar_state, _board->isExternalPowered() ? "yes" : "no", _board->isChargerActive() ? "yes" : "no"); } else if (memcmp(command, "sensor get ", 11) == 0) { From 2940ead5049604e75fcedf963a3b3cfb94ea9ee2 Mon Sep 17 00:00:00 2001 From: F4FPR Date: Tue, 18 Aug 2026 19:40:42 +0200 Subject: [PATCH 037/174] Update ThinkNodeM6Board.h --- variants/thinknode_m6/ThinkNodeM6Board.h | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 1af952d2..934794c9 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -36,19 +36,33 @@ public: return "Elecrow ThinkNode M6"; } + bool hasUsbPowerDetect() const override { return true; } + + bool isUsbPowered() override { + // P0.13 is the dedicated USB / external power-detect line on the M6 PCB + // (same pin the Meshtastic M6 variant uses for USB detection). + return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE; + } + + bool hasSolarChargerDetect() const override { return true; } + + bool isSolarChargerActive() override { + // P0.15 is the active-low solar-charger status line. It reports the + // charger's state, not photovoltaic voltage or current. + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE; + } + bool isExternalPowered() override { - if (digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE || - digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE) { + if (isUsbPowered() || isSolarChargerActive()) { return true; } return NRF52Board::isExternalPowered(); } bool isChargerActive() override { - // The charge-status line covers solar input; external USB input is also a - // charging source, although the board cannot report charge completion. - return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || - digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE; + // Preserve a generic charging indication while also exposing both + // source-specific states (USB / solar) separately through the CLI. + return isSolarChargerActive() || isUsbPowered(); } void powerOff() override { From 612b217b90a52ebd1c88218b25584fac82da62c0 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 12:07:29 +1200 Subject: [PATCH 038/174] updated gcc toolchain for nrf52 boards --- platformio.ini | 1 + src/armstubs.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 src/armstubs.cpp diff --git a/platformio.ini b/platformio.ini index e78124a4..ee502473 100644 --- a/platformio.ini +++ b/platformio.ini @@ -86,6 +86,7 @@ platform_packages = ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM diff --git a/src/armstubs.cpp b/src/armstubs.cpp new file mode 100644 index 00000000..30da40ec --- /dev/null +++ b/src/armstubs.cpp @@ -0,0 +1,9 @@ +// https://github.com/meshcore-dev/MeshCore/issues/2469 +// armstubs.cpp exists to silence warnings introduced after upgrading platformio/toolchain-gccarmnoneeabi +extern "C" __attribute__((weak)) int _close(int fd) { return -1; } +extern "C" __attribute__((weak)) int _lseek(int fd, int offset, int whence) { return 0; } +extern "C" __attribute__((weak)) int _read(int fd, char *buf, int len) { return 0; } +extern "C" __attribute__((weak)) int _fstat(int fd, struct stat *st) { return 0; } +extern "C" __attribute__((weak)) int _isatty(int fd) { return 1; } +extern "C" __attribute__((weak)) int _getpid(void) { return 1; } +extern "C" __attribute__((weak)) int _kill(int pid, int sig) { return -1; } From 4b8966d4f9496cc6b5b83c434bb6d7e5027cdb8b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 19 Aug 2026 15:29:19 +1000 Subject: [PATCH 039/174] WIO Tracker L1 1W variant --- examples/companion_radio/MyMesh.cpp | 4 + .../wio-tracker-l1-1w/WioTrackerL1Board.cpp | 40 ++++++ .../wio-tracker-l1-1w/WioTrackerL1Board.h | 46 +++++++ variants/wio-tracker-l1-1w/platformio.ini | 121 ++++++++++++++++ variants/wio-tracker-l1-1w/target.cpp | 40 ++++++ variants/wio-tracker-l1-1w/target.h | 35 +++++ variants/wio-tracker-l1-1w/variant.cpp | 78 +++++++++++ variants/wio-tracker-l1-1w/variant.h | 130 ++++++++++++++++++ 8 files changed, 494 insertions(+) create mode 100644 variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp create mode 100644 variants/wio-tracker-l1-1w/WioTrackerL1Board.h create mode 100644 variants/wio-tracker-l1-1w/platformio.ini create mode 100644 variants/wio-tracker-l1-1w/target.cpp create mode 100644 variants/wio-tracker-l1-1w/target.h create mode 100644 variants/wio-tracker-l1-1w/variant.cpp create mode 100644 variants/wio-tracker-l1-1w/variant.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16603bc4..a6739ac0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -886,7 +886,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default _prefs.radio_fem_rxgain = 1; +#ifdef RADIO_FEM_TXGAIN + _prefs.radio_fem_txgain = RADIO_FEM_TXGAIN; // board-specific default for PA +#else _prefs.radio_fem_txgain = 0; +#endif //_prefs.rx_delay_base = 10.0f; enable once new algo fixed _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp new file mode 100644 index 00000000..8210c24e --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include "WioTrackerL1Board.h" + +void WioTrackerL1Board::begin() { + NRF52BoardDCDC::begin(); + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input + // Set all button pins to INPUT_PULLUP + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { + _is_pa_enabled = enable; + digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA + return true; +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h new file mode 100644 index 00000000..c5a60944 --- /dev/null +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +class WioTrackerL1Board : public NRF52BoardDCDC { +protected: + uint8_t btn_prev_state; + bool _is_pa_enabled = false; + +public: + WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + void begin(); + +#if defined(P_LORA_TX_LED) + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + } +#endif + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + bool canControlLoRaFemPaGain() const override { return true; } + bool setLoRaFemPaGainEnabled(bool enable) override; + bool isLoRaFemPaGainEnabled() const override { return _is_pa_enabled; } + + void powerOff() override { + setLoRaFemPaGainEnabled(false); // turn Off PA + NRF52Board::powerOff(); + } +}; diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini new file mode 100644 index 00000000..e5b4a3be --- /dev/null +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -0,0 +1,121 @@ +[WioTrackerL1-1W] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1-1w + -D WIO_TRACKER_L1 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_OLED_RESET=-1 + -D GPS_BAUD_RATE=9600 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1-1w> + + + + +lib_deps= ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + +[env:WioTrackerL1-1W_repeater] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_room_server] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1-1W.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1-1W_companion_radio_usb] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ENABLE_USB_INTERFACE +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + + + +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_companion_radio_ble] +extends = WioTrackerL1-1W +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${WioTrackerL1-1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display + -D UI_HAS_JOYSTICK=1 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D RADIO_FEM_TXGAIN=1 + -D ADVERT_NAME='"@@MAC"' + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${WioTrackerL1-1W.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1-1W_kiss_modem] +extends = WioTrackerL1-1W +build_src_filter = ${WioTrackerL1-1W.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/wio-tracker-l1-1w/target.cpp b/variants/wio-tracker-l1-1w/target.cpp new file mode 100644 index 00000000..7a573258 --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.cpp @@ -0,0 +1,40 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + diff --git a/variants/wio-tracker-l1-1w/target.h b/variants/wio-tracker-l1-1w/target.h new file mode 100644 index 00000000..05bc7ff1 --- /dev/null +++ b/variants/wio-tracker-l1-1w/target.h @@ -0,0 +1,35 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #if defined(WIO_TRACKER_L1_EINK) + #include + #else + #include + #endif + #include +#endif +#include + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/wio-tracker-l1-1w/variant.cpp b/variants/wio-tracker-l1-1w/variant.cpp new file mode 100644 index 00000000..18efe7ea --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.cpp @@ -0,0 +1,78 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 29, // D5 P0.29 (AIN5) LORA_VDET 鈥?Pro 1W: P0.29 (not P1.08) + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.01 Mesh_LED (orange) 鈥?Pro 1W: P1.01 (not P1.15) + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL + + // D31-D33 - Pro 1W only + 13, // D31 P0.13 BOOST_EN (Grove 5V Boost) + 47, // D32 P1.15 nRF_Sig_Charge_State (BQ25616 STAT) + 14, // D33 P0.14 LORA_PWR_EN (SX1262 + 1 W PA LDO) +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1-1w/variant.h b/variants/wio-tracker-l1-1w/variant.h new file mode 100644 index 00000000..7520460a --- /dev/null +++ b/variants/wio-tracker-l1-1w/variant.h @@ -0,0 +1,130 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (34) +#define NUM_DIGITAL_PINS (34) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (29) // Joystick Press +#define PIN_BACK_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 +#define PIN_USER_BTN PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// BQ25616 single-wire charge status (Pro 1W) +#define PIN_BOOST_EN (31) // D31 / P0.13 鈥?Grove 5V Boost enable +#define EXT_CHRG_DETECT (32) // D32 / P1.15 鈥?BQ25616 STAT +#define EXT_CHRG_DETECT_VALUE LOW // 0 = charging, 1 = full / charger sleep +#define BOOST_EN_ACTIVE HIGH // HIGH enables Grove 5V Boost + +// Lora Pins +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_DIO_1 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) + +// LORA_PWR_EN (P0.14): external LDO enable for the SX1262 + 1 W PA on the Pro 1W board. +#define LORA_PWR_EN (33) +#define SX126X_POWER_EN LORA_PWR_EN + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (18) +#define PIN_WIRE1_SCL (17) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (PIN_GPS_STANDBY) + +// QSPI Pins +#define PIN_QSPI_SCK (19) +#define PIN_QSPI_CS (20) +#define PIN_QSPI_IO0 (21) +#define PIN_QSPI_IO1 (22) +#define PIN_QSPI_IO2 (23) +#define PIN_QSPI_IO3 (24) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// // EInk on SPI1 +// #define PIN_DISPLAY_CS (36) +// #define PIN_DISPLAY_BUSY (35) +// #define PIN_DISPLAY_DC (34) +// #define PIN_DISPLAY_RST (32) + +#define PIN_SPI1_MISO (37) +#define PIN_SPI1_MOSI (33) +#define PIN_SPI1_SCK (31) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +#endif \ No newline at end of file From 67d6d42a4c1cc57196e00817a2c2271dffe24cc6 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 19 Aug 2026 18:59:24 +1200 Subject: [PATCH 040/174] fix for 3-byte paths passed to CMD_SEND_RAW_DATA --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 16603bc4..fdece482 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1511,7 +1511,7 @@ void MyMesh::handleCmdFrame(size_t len) { #endif } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { int i = 1; - int8_t path_len = cmd_frame[i++]; + uint8_t path_len = cmd_frame[i++]; if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) { uint8_t path[MAX_PATH_SIZE]; i += mesh::Packet::writePath(path, &cmd_frame[i], path_len); From b76242043399cd5a3a4bfe1e36b505a69165ab25 Mon Sep 17 00:00:00 2001 From: Thomas Osterried Date: Wed, 19 Aug 2026 09:35:25 +0200 Subject: [PATCH 041/174] Skip empty channel slots in searchChannelsByHash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unconfigured slot has an all-zero secret, so it matches null-key group traffic (a sender with an unset PSK). The zero-key MAC validates against the empty slot and the foreign message is delivered as if it belonged to that channel — every node with a free slot is a null-key sink. Skip empty slots. --- src/helpers/BaseChatMesh.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9..616ee39b 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -368,6 +368,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8 int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Skip empty/unconfigured slots. An empty slot has an all-zero secret and + // therefore matches null-key group traffic (a node transmitting with an + // unset PSK): the zero-key MAC validates against the empty slot and the + // foreign message is delivered as if it belonged to that channel. Any node + // with a free channel slot would otherwise act as a null-key sink. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } From 3a440ac41af0e19f2f79aba7b5efd16a80988963 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:06:43 -0400 Subject: [PATCH 042/174] Add charging indicator to companion_radio battery icon Show a small lightning-bolt icon to the left of the battery indicator on the ui-new home screen while the device is externally powered, and a plug icon once the battery reads full. The bolt/plug sits beside the battery so the fill bar stays clean and uninterrupted. When a buzzer is present, the mute icon shifts one slot further left so the two never overlap. Charging state is derived from board.isExternalPowered(), so this works on any board that reports external power (e.g. the nRF52 VBUS-detect path). Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 14 ++++++++++++-- examples/companion_radio/ui-new/icons.h | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 59b58461..617f5e87 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -146,11 +146,21 @@ class HomeScreen : public UIScreen { int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); - // show muted icon if buzzer is muted + // while charging, show a bolt (or a plug once full) just left of the battery, + // keeping the fill bar itself clean and uninterrupted + bool charging = board.isExternalPowered(); + if (charging) { + const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + display.setColor(UIColor::title_txt); + display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); + } + + // show muted icon if buzzer is muted (shifted further left when the charging + // icon already occupies the slot immediately left of the battery) #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { display.setColor(UIColor::warning_txt); - display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8); } #endif } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe23790..8d9ab175 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 +}; + +// small lightning bolt, 8x8px, shown next to the battery icon while charging +static const uint8_t charging_icon[] = { + 0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0 +}; + +// small power plug, 8x8px, shown next to the battery icon once fully charged +static const uint8_t plug_icon[] = { + 0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18 }; \ No newline at end of file From 41c60da5759ccb2c0a04e3b7a354e66e51c2de03 Mon Sep 17 00:00:00 2001 From: hansimgamr <77758818+hansimgamr@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:14:12 -0400 Subject: [PATCH 043/174] Show plug at >=95% instead of exactly 100% Boards without a charge-complete signal only infer "full" from voltage, and a real pack rarely reads the full BATT_MAX_MILLIVOLTS (4.2V), so the plug icon was effectively never shown. Treat "full" as a high band (>= 95%) so the plug appears when the battery is charged rather than requiring an exact 100%. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/ui-new/UITask.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 617f5e87..55755ef1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -150,7 +150,10 @@ class HomeScreen : public UIScreen { // keeping the fill bar itself clean and uninterrupted bool charging = board.isExternalPowered(); if (charging) { - const uint8_t* symbol = (batteryPercentage < 100) ? charging_icon : plug_icon; + // There's no charge-complete signal on most boards, so "full" is a high + // voltage band rather than an exact 100% (a real pack rarely reads 4.2V). + const int BATT_FULL_PCT = 95; + const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon; display.setColor(UIColor::title_txt); display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); } From afb969ccca2f06913aec207a3cd4f2910806fabc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 21 Aug 2026 20:55:57 +1000 Subject: [PATCH 044/174] * initial wiring/routing of CLI commands to companion (either via app/serial interface, or remotely via txt message) --- examples/companion_radio/MyMesh.cpp | 57 ++++++++++++++++++++++------ examples/companion_radio/MyMesh.h | 6 ++- examples/simple_secure_chat/main.cpp | 2 +- src/Utils.cpp | 9 +++++ src/Utils.h | 2 + src/helpers/BaseChatMesh.cpp | 43 +++++++++++++++------ src/helpers/BaseChatMesh.h | 2 +- src/helpers/ContactInfo.h | 6 +++ 8 files changed, 101 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fdece482..49c9f91b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,6 +62,7 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 +#define CMD_RUN_CLI_COMMAND 66 // v14+ // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -97,6 +98,7 @@ #define RESP_ALLOWED_REPEAT_FREQ 26 #define RESP_CODE_CHANNEL_DATA_RECV 27 #define RESP_CODE_DEFAULT_FLOOD_SCOPE 28 +#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND #define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9) @@ -529,9 +531,13 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t } void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) { + const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + if (from.isRemoteCLIAllowed() && handleCommand(text, sender_timestamp, reply)) { + // CLI command was handled. Let BaseChatMesh handle the sending of the reply + } else { + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + } } void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, @@ -1082,6 +1088,21 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], _prefs.node_name, tlen); i += tlen; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+ + int i = 1; + char *text = (char *)&cmd_frame[i]; + int tlen = len - i; + text[tlen] = 0; // ensure null + + reply_buf[0] = 0; + if (handleCommand(text, 0, reply_buf)) { + out_frame[0] = RESP_CODE_CLI_REPLY; + int rlen = strlen(reply_buf); + memcpy(&out_frame[1], reply_buf, rlen); + _serial->writeFrame(out_frame, 1 + rlen); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // unsupported command + } } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { int i = 1; uint8_t txt_type = cmd_frame[i++]; @@ -2031,6 +2052,25 @@ void MyMesh::enterCLIRescue() { Serial.println("========= CLI Rescue ========="); } +bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + *reply = 0; + command += 3; + } + + if (memcmp(command, "set pin ", 8) == 0) { + _prefs.ble_pin = atoi(&command[8]); + savePrefs(); + sprintf(reply, "> pin is now %06d", _prefs.ble_pin); + return true; + } + return false; // not handled +} + void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); while (Serial.available() && len < sizeof(cli_command)-1) { @@ -2048,15 +2088,10 @@ void MyMesh::checkCLIRescueCmd() { if (len > 0 && cli_command[len - 1] == '\r') { // received complete line cli_command[len - 1] = 0; // replace newline with C string null terminator - if (memcmp(cli_command, "set ", 4) == 0) { - const char* config = &cli_command[4]; - if (memcmp(config, "pin ", 4) == 0) { - _prefs.ble_pin = atoi(&config[4]); - savePrefs(); - Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); - } else { - Serial.printf(" Error: unknown config: %s\n", config); - } + reply_buf[0] = 0; + if (handleCommand(cli_command, 0, reply_buf)) { + // command was handled, print reply output + Serial.print(" "); Serial.print(reply_buf); Serial.println(); } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada..9c479c15 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 13 +#define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "14 Aug 2026" @@ -134,7 +134,7 @@ protected: void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) override; + const char *text, char* reply) override; void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, @@ -201,6 +201,7 @@ private: } void checkCLIRescueCmd(); + bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; @@ -225,6 +226,7 @@ private: bool _cli_rescue; bool send_unscoped; // force un-scoped flood (instead of using send_scope) char cli_command[80]; + char reply_buf[166]; uint8_t app_target_ver; uint8_t *sign_data; uint32_t sign_data_len; diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index da42ddcb..241fe1c2 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -240,7 +240,7 @@ protected: } } - void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { + void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { } void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { } diff --git a/src/Utils.cpp b/src/Utils.cpp index 5ae7f0e2..7a3fb78b 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -203,6 +203,15 @@ bool Utils::isHexChar(char c) { return c == '0' || hexVal(c) > 0; } +bool Utils::isZeroes(const uint8_t* buf, size_t len) { + while (len > 0) { + if (*buf != 0) return false; + buf++; + len--; + } + return true; +} + bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) { int len = strlen(src_hex); if (len != dest_size*2) return false; // incorrect length diff --git a/src/Utils.h b/src/Utils.h index 5736b874..7a0f7b6e 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -82,6 +82,8 @@ public: static int parseTextParts(char* text, const char* parts[], int max_num, char separator=','); static bool isHexChar(char c); + + static bool isZeroes(const uint8_t* buf, size_t len); }; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 616ee39b..bf8a861c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,6 +9,8 @@ #define TXT_ACK_DELAY 200 #endif +#define CLI_REPLY_DELAY_MILLIS 600 + void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { sendFlood(pkt, delay_millis); } @@ -227,8 +229,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender ContactInfo& from = contacts[i]; if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { - uint32_t timestamp; - memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = data[4] >> 2; // message attempt number, and other flags // len can be > original length, but 'text' will be padded with zeroes @@ -236,7 +238,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (flags == TXT_TYPE_PLAIN) { from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know int text_len = strlen((char *)&data[5]); uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it @@ -254,20 +256,39 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { - onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know + uint8_t temp[166]; + char *command = (char *)&data[5]; + char *reply = (char *)&temp[5]; + *reply = 0; + + onCommandDataRecv(from, packet, sender_timestamp, command, reply); // let UI know // NOTE: no ack expected for CLI_DATA replies - if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) - mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0); - if (path) sendFloodScoped(from, path); + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + timestamp++; + } + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len); + if (reply_pkt) { + if (from.out_path_len == OUT_PATH_UNKNOWN) { + sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { - if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date - from.sync_since = timestamp; + if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date + from.sync_since = sender_timestamp; } from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time - onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know + onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d9878547..331d1041 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -113,7 +113,7 @@ protected: virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len); virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; - virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0; virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index ede977ca..5a156c82 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -26,6 +26,12 @@ struct ContactInfo { return shared_secret; } + bool isFav() const { return flags & 0x01; } + bool isTelemBaseAllowed() const { return flags & 0x02; } + bool isTelemLocAllowed() const { return flags & 0x04; } + bool isTelemEnvAllowed() const { return flags & 0x08; } + bool isRemoteCLIAllowed() const { return flags & 0x10; } + private: mutable uint8_t shared_secret[PUB_KEY_SIZE]; }; From e749010bbc9ab61215c3d73d3e35d60058d23929 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:01:52 +1000 Subject: [PATCH 045/174] Duplicate MAX_CLIENTS definition MAX_CLIENTS is defined in both src/helpers/ClientACL.h and examples/simple_repeater/MyMesh.h. Appears it was centralised in the former header file some time ago. Both are included in some places and, depending on which order they're in, either value can win. This change drops the duplicate entry from the repeater firmware and bumps the central limit up to 32 (per the original repeater firmware value). Changes: - Remove MAX_CLIENTS from repeater MyMesh.h - Increase MAX_CLIENTS limit in ClientACL.h to 32 --- examples/simple_repeater/MyMesh.h | 4 ---- src/helpers/ClientACL.h | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb9..cac6c4a2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -59,10 +59,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f706..e0654464 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { From 52f0362c09aec05323803cada70f17c8efb83eee Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 20 Aug 2026 21:17:17 -0400 Subject: [PATCH 046/174] ui: repeater discover screen --- examples/companion_radio/MyMesh.cpp | 50 ++++++++++++++++++++ examples/companion_radio/MyMesh.h | 18 ++++++++ examples/companion_radio/ui-new/UITask.cpp | 53 +++++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index fdece482..01512b16 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -134,6 +134,10 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 +// Copied from simple_repeater (could probably be shared) +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -403,6 +407,51 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { + if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; + if (max_num > disc_nodes_count) max_num = disc_nodes_count; + + for (int i = 0; i < max_num; i++) { + nodes[i] = discovered_nodes[i]; + } + return max_num; +} + +bool MyMesh::requestRepeatersDiscovery() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + getRNG()->random(&cmd_bytes[2], 4); // tag + disc_nodes_count = 0; + disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); + mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + sendZeroHop(req); + return true; + } + return false; +} + +void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { + if ((p_len < 12) + || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) + || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) + || (memcmp(&payload[2], &disc_node_req_tag, 4))) { + return; + } + memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); + discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; + discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; + discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); + ContactInfo* c = lookupContactByPubKey(&payload[6], 8); + if (c != NULL) { + strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); + } else { + discovered_nodes[disc_nodes_count].name[0] = 0; + } + disc_nodes_count ++; +} + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -783,6 +832,7 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } + checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada..848d21c0 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,14 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +struct DiscoveredNode { + uint8_t pubkey_prefix[9]; + float snr_in; + float snr_out; + char name[32]; + uint8_t type; +}; + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); @@ -102,6 +110,9 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); + bool requestRepeatersDiscovery(); + int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); + protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -256,6 +267,13 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table + + #define DISCOVERED_NODES_TABLE_SIZE 10 + DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; + + void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef1..35525028 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -24,6 +24,7 @@ #define LONG_PRESS_MILLIS 1200 +// Used both for recent adverts and discovered nodes #ifndef UI_RECENT_LIST_SIZE #define UI_RECENT_LIST_SIZE 4 #endif @@ -102,6 +103,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif + DISCOVERY, SHUTDOWN, Count // keep as last }; @@ -113,7 +115,9 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - + DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; + uint32_t discovery_req_time = 0; + bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -459,6 +463,39 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif + } else if (_page == HomePage::DISCOVERY) { + int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + display.setColor(UIColor::primary_txt); + int y = 20; + for (int i = 0; i < count; i++, y += 11) { + char name[32]; + auto a = &discovered[i]; + if ((a->name[0] == 0) || !discovery_disp_names) { + mesh::Utils::toHex(name, a->pubkey_prefix, 4); + } else { + strncpy(name, a->name, 32); + } + char filtered_name[sizeof(name)]; + char snr_s[12]; + if (strlen(name) <= 8) { // display snr_o + sprintf(snr_s, "%02.1f>%02.1f", a->snr_out, a->snr_in); + } else { + sprintf(snr_s, "%02.1f", a->snr_in); + } + int snr_width = display.getTextWidth(snr_s); + int max_name_width = display.width() - snr_width - 1; + display.translateUTF8ToBlocks(filtered_name, name, sizeof(filtered_name)); + display.drawTextEllipsized(0, y, max_name_width, filtered_name); + display.setCursor(display.width() - snr_width - 1, y); + display.print(snr_s); + } + if (millis() < discovery_req_time + 5000) { + return 1000; // more frequent updates just after req + } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * UI_RECENT_LIST_SIZE; + display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); + } + } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -484,6 +521,9 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } + if (_page == HomePage::DISCOVERY) { + _task->showAlert("Repeater disc", 800); + } return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -516,6 +556,17 @@ public: return true; } #endif + if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { + if (millis() > discovery_req_time + 5000) { // rate limiter + the_mesh.requestRepeatersDiscovery(); + discovery_req_time = millis(); + } + return true; + } + if (c == KEY_SELECT && _page == HomePage::DISCOVERY) { + discovery_disp_names = !discovery_disp_names; + return true; + } if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; From 0b652b56294d2d6a23c7ca62b14b8215b1ccca4a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sat, 22 Aug 2026 16:16:32 +1200 Subject: [PATCH 047/174] add support for rak12500 and rak12501 gps on rak3401 --- variants/rak3401/platformio.ini | 1 + variants/rak3401/variant.h | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e71922..c285e3ec 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 + -D RAK_BOARD -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 98827886..e0c24759 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 From ab4c33826dbcaf80aad89ac94048acc104e553d5 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 23 Aug 2026 14:14:45 +1200 Subject: [PATCH 048/174] don't toggle io pins on rak3401 when probing gps --- src/helpers/sensors/EnvironmentSensorManager.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 54305692..6f460775 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -831,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ } #endif + #ifndef RAK_3401 //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); delay(500); digitalWrite(ioPin,HIGH); + #endif + + // give gps time to power up delay(500); //Try to init RAK12500 on I2C @@ -871,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ return true; } + #ifndef RAK_3401 pinMode(ioPin, INPUT); + #endif MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } @@ -880,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, HIGH); + #endif return; #endif @@ -896,8 +904,10 @@ void EnvironmentSensorManager::start_gps() { void EnvironmentSensorManager::stop_gps() { gps_active = false; #ifdef RAK_WISBLOCK_GPS + #ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA pinMode(gpsResetPin, OUTPUT); digitalWrite(gpsResetPin, LOW); + #endif return; #endif From 41588d805253fd4d4852354efd4f369d99ed3496 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 14:45:15 +1000 Subject: [PATCH 049/174] * adding new CommonRadioPrefs * refactor: moving various radio CLI handling to CommonRadioPrefs --- examples/companion_radio/MyMesh.cpp | 26 +++++++++++++ examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/NodePrefs.h | 18 ++++++++- src/helpers/AdvertDataHelpers.cpp | 8 ++++ src/helpers/AdvertDataHelpers.h | 2 + src/helpers/CommonCLI.cpp | 51 +++++-------------------- src/helpers/CommonCLI.h | 17 ++++++++- src/helpers/CommonRadioPrefs.cpp | 56 ++++++++++++++++++++++++++++ src/helpers/CommonRadioPrefs.h | 45 ++++++++++++++++++++++ 9 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 src/helpers/CommonRadioPrefs.cpp create mode 100644 src/helpers/CommonRadioPrefs.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 49c9f91b..54be91c7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2062,12 +2062,38 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* command += 3; } + if (_prefs.getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is radio CLI command? + if (_prefs.getRadioPrefs()->isDirty()) { savePrefs(); } + return true; + } + + if (memcmp(command, "set name ", 9) == 0) { + if (AdvertDataParser::isValidName(&command[9])) { + StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, bad chars"); + } + return true; + } + if (strcmp(command, "get name") == 0) { + sprintf(reply, "> %s", _prefs.node_name); + return true; + } + if (memcmp(command, "set pin ", 8) == 0) { _prefs.ble_pin = atoi(&command[8]); savePrefs(); sprintf(reply, "> pin is now %06d", _prefs.ble_pin); return true; } + + if (strcmp(command, "ver") == 0) { + sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); + return true; + } + return false; // not handled } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 9c479c15..b60030a6 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -169,6 +169,7 @@ public: _prefs.node_lat = sensors.node_lat; _prefs.node_lon = sensors.node_lon; _store->savePrefs(_prefs); + _prefs.clearDirty(); } #if ENV_INCLUDE_GPS == 1 diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 21766de8..36788052 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,7 @@ #pragma once #include // For uint8_t, uint32_t #include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -42,7 +43,7 @@ public: uint8_t default_scope_key[16]; private: - class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -70,6 +71,18 @@ private: } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + + // CommonRadioPrefs interface + float getFreq() const override { return _parent->freq; } + void setFreq(float f) override { _parent->freq = f; markDirty(); } + float getBandwidth() const override { return _parent->bw; } + void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } + uint8_t getSpreadFactor() const override { return _parent->sf; } + void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + uint8_t getCodingRate() const override { return _parent->cr; } + void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + float getAirtimeFactor() const override { return _parent->airtime_factor; } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } }; RadioPrefs radio; @@ -142,4 +155,7 @@ public: // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } + + CommonRadioPrefs* getRadioPrefs() { return &radio; } + void clearDirty() { radio.clearDirty(); } }; diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 998733ae..25e5cd3f 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -28,6 +28,14 @@ return i; } +bool AdvertDataParser::isValidName(const char *n) { + while (*n) { + if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; + n++; + } + return true; +} + AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) { _name[0] = 0; _lat = _lon = 0; diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index abe14cbd..f4e109e9 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -50,6 +50,8 @@ class AdvertDataParser { public: AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len); + static bool isValidName(const char* name); + bool isValid() const { return _valid; } uint8_t getType() const { return _flags & 0x0F; } uint16_t getFeat1() const { return _extra1; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..4e7af30b 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -164,6 +164,7 @@ void CommonCLI::savePrefs() { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } _callbacks->savePrefs(); + _prefs->clearDirty(); } uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { @@ -180,6 +181,11 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { } void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + if (_prefs->getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is a radio CLI command? + if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); } + return; + } + if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { @@ -447,19 +453,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "dutycycle ", 10) == 0) { - float dc = atof(&config[10]); - if (dc < 1 || dc > 100) { - strcpy(reply, "ERROR: dutycycle must be 1-100"); - } else { - _prefs->airtime_factor = (100.0f / dc) - 1.0f; - savePrefs(); - float actual = 100.0f / (_prefs->airtime_factor + 1.0f); - int a_int = (int)actual; - int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); - sprintf(reply, "OK - %d.%d%%", a_int, a_frac); - } - } else if (memcmp(config, "af ", 3) == 0) { + + if (memcmp(config, "af ", 3) == 0) { _prefs->airtime_factor = atof(&config[3]); savePrefs(); strcpy(reply, "OK"); @@ -585,24 +580,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error: state must be on or off"); } - } else if (memcmp(config, "radio ", 6) == 0) { - strcpy(tmp, &config[6]); - const char *parts[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); - float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); @@ -806,12 +783,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "dutycycle", 9) == 0) { - float dc = 100.0f / (_prefs->airtime_factor + 1.0f); - int dc_int = (int)dc; - int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); - sprintf(reply, "> %d.%d%%", dc_int, dc_frac); - } else if (memcmp(config, "af", 2) == 0) { + if (memcmp(config, "af", 2) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); @@ -856,11 +828,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); } - } else if (memcmp(config, "radio", 5) == 0) { - char freq[16], bw[16]; - strcpy(freq, StrHelper::ftoa(_prefs->freq)); - strcpy(bw, StrHelper::ftoa3(_prefs->bw)); - sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); } else if (memcmp(config, "rxdelay", 7) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); } else if (memcmp(config, "txdelay", 7) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e..9d3caf39 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -6,6 +6,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -72,7 +73,7 @@ public: uint8_t extra_sf[4]; private: - class RadioPrefs : public ConfigSerializer { + class RadioPrefs : public CommonRadioPrefs { NodePrefs* _parent; protected: void structure() override { @@ -96,6 +97,17 @@ private: } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } + // CommonRadioPrefs interface + float getFreq() const override { return _parent->freq; } + void setFreq(float f) override { _parent->freq = f; markDirty(); } + float getBandwidth() const override { return _parent->bw; } + void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } + uint8_t getSpreadFactor() const override { return _parent->sf; } + void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + uint8_t getCodingRate() const override { return _parent->cr; } + void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + float getAirtimeFactor() const override { return _parent->airtime_factor; } + void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } }; RadioPrefs radio; @@ -192,6 +204,9 @@ public: bridge_secret[0] = 0; owner_info[0] = 0; } + + CommonRadioPrefs* getRadioPrefs() { return &radio; } + void clearDirty() { radio.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp new file mode 100644 index 00000000..ed198c61 --- /dev/null +++ b/src/helpers/CommonRadioPrefs.cpp @@ -0,0 +1,56 @@ +#include "CommonRadioPrefs.h" +#include "TxtDataHelpers.h" +#include "Utils.h" + +bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio") == 0) { + char freq[16], bw[16]; + strcpy(freq, StrHelper::ftoa(getFreq())); + strcpy(bw, StrHelper::ftoa3(getBandwidth())); + sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate()); + return true; + } + if (memcmp(command, "set radio ", 10) == 0) { + char tmp[132]; + strcpy(tmp, &command[10]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; + float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; + uint8_t sf = num > 2 ? atoi(parts[2]) : 0; + uint8_t cr = num > 3 ? atoi(parts[3]) : 0; + if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { + setSpreadFactor(sf); + setCodingRate(cr); + setFreq(freq); + setBandwidth(bw); + // NOTE: savePrefs() should be handled by caller + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error, invalid radio params"); + } + return true; + } + if (strcmp(command, "get dutycycle") == 0) { + float dc = 100.0f / (getAirtimeFactor() + 1.0f); + int dc_int = (int)dc; + int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f); + sprintf(reply, "> %d.%d%%", dc_int, dc_frac); + return true; + } + if (memcmp(command, "set dutycycle ", 14) == 0) { + float dc = atof(&command[14]); + if (dc < 1 || dc > 100) { + strcpy(reply, "ERROR: dutycycle must be 1-100"); + } else { + setAirtimeFactor((100.0f / dc) - 1.0f); + // NOTE: savePrefs() should be handled by caller + float actual = 100.0f / (getAirtimeFactor() + 1.0f); + int a_int = (int)actual; + int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); + sprintf(reply, "OK - %d.%d%%", a_int, a_frac); + } + return true; + } + return false; // not handled +} diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h new file mode 100644 index 00000000..eaec2c0a --- /dev/null +++ b/src/helpers/CommonRadioPrefs.h @@ -0,0 +1,45 @@ +#include "ConfigSerializer.h" + +class CommonRadioPrefs : public ConfigSerializer { + bool _is_dirty = false; +protected: + CommonRadioPrefs() { } +public: + void markDirty() { _is_dirty = true; } + void clearDirty() { _is_dirty = false; } + bool isDirty() const { return _is_dirty; } + + virtual float getFreq() const = 0; + virtual void setFreq(float f) = 0; + + virtual float getBandwidth() const = 0; + virtual void setBandwidth(float bw) = 0; + + virtual uint8_t getSpreadFactor() const = 0; + virtual void setSpreadFactor(uint8_t sf) = 0; + + virtual uint8_t getCodingRate() const = 0; + virtual void setCodingRate(uint8_t cr) = 0; + + virtual float getAirtimeFactor() const = 0; + virtual void setAirtimeFactor(float af) = 0; + + // //def("cad", _parent->cad_enabled); + // //def("int_thr", _parent->interference_threshold); + // def("rxgain", _parent->rx_boosted_gain); + // #if 0 + // // NOTE: these cannot be set (yet) so don't load/save until we can. + // // also, fem_rxgain WAS mapped to wrong JSON property previously + // def("fem_rxgain", _parent->radio_fem_rxgain); + // def("fem_txgain", _parent->radio_fem_txgain); + // #endif + // def("tx", _parent->tx_power_dbm); + // def("rxdelay", _parent->rx_delay_base); + // //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded + // //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded + // //def("agc_int", _parent->agc_reset_interval); + // def("hash_mode", _parent->path_hash_mode); + // def("multi_ack", _parent->multi_acks); + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); +}; From b4f7e941fa91d9d1180ac0086fefe60def1263e9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 17:30:30 +1000 Subject: [PATCH 050/174] * CommonRadioPrefs refactors done --- examples/companion_radio/NodePrefs.h | 24 ++++- src/helpers/CommonCLI.cpp | 99 +----------------- src/helpers/CommonCLI.h | 24 ++++- src/helpers/CommonRadioPrefs.cpp | 147 ++++++++++++++++++++++++++- src/helpers/CommonRadioPrefs.h | 45 +++++--- 5 files changed, 220 insertions(+), 119 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 36788052..f1cb69cc 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -78,11 +78,31 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } - void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return false; } + void setCadEnabled(bool en) override { /* no-op */ } + uint8_t getIntThresh() const override { return 0; } + void setIntThresh(uint8_t t) override { /* no-op */ } + uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } + void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } + uint8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + float getRxDelay() const override { return _parent->rx_delay_base; } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint8_t getAgcResetInt() const override { return 0; } + void setAgcResetInt(uint8_t secs) override { /* no-op */ } + uint8_t getHashMode() const override { return _parent->path_hash_mode; } + void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } + uint8_t getMultiAcks() const override { return _parent->multi_acks; } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return 0.5f; } // currently hard-coded + void setFloodTxDelay(float d) override { /* no-op */ } + float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded + void setDirectTxDelay(float d) override { /* no-op */ } }; RadioPrefs radio; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 4e7af30b..2e9efa9c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -454,27 +454,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "cad ", 4) == 0) { - _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { - _prefs->agc_reset_interval = atoi(&config[19]) / 4; - savePrefs(); - sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { + if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); @@ -527,15 +507,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); - } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - bool enabled = memcmp(&config[13], "on", 2) == 0; - _prefs->rx_boosted_gain = enabled; - savePrefs(); - if (_callbacks->setRxBoostedGain(enabled)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: unsupported"); - } } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); @@ -588,24 +559,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->node_lon = atof(&config[4]); savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0 && db <= 20.0f) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-20"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0 && f <= 2.0f) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { uint8_t m = atoi(&config[19]); if (m <= 64) { @@ -633,15 +586,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0 && f <= 2.0f) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-2"); - } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -652,16 +596,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep *dp = 0; savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } } else if (memcmp(config, "loop.detect ", 12) == 0) { config += 12; uint8_t mode; @@ -682,11 +616,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); strcpy(reply, "OK"); } - } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); - savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); savePrefs(); @@ -783,17 +712,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) { const char* config = &command[4]; - if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); - } else if (memcmp(config, "int.thresh", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); - } else if (memcmp(config, "cad", 3) == 0) { - sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); - } else if (memcmp(config, "agc.reset.interval", 18) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); - } else if (memcmp(config, "allow.read.only", 15) == 0) { + if (memcmp(config, "allow.read.only", 15) == 0) { sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); } else if (memcmp(config, "flood.advert.interval", 21) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); @@ -814,8 +733,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.rxgain", 12) == 0) { - sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { if (!_board->canControlLoRaFemLna()) { strcpy(reply, "Error: unsupported"); @@ -828,18 +745,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); } - } else if (memcmp(config, "rxdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); - } else if (memcmp(config, "txdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "direct.txdelay", 14) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; @@ -850,8 +761,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sp++; } *reply = 0; // set null terminator - } else if (memcmp(config, "path.hash.mode", 14) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); @@ -862,10 +771,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "> strict"); } - } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { - sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); - } else if (memcmp(config, "freq", 4) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 9d3caf39..69243f52 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -103,11 +103,31 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } - void setCodingRate(uint8_t cr) { _parent->cr = cr; markDirty(); } + void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled; markDirty(); } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } + uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } + void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } + uint8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + float getRxDelay() const override { return _parent->rx_delay_base; } + void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint8_t getHashMode() const override { return _parent->path_hash_mode; } + void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } + uint8_t getMultiAcks() const override { return _parent->multi_acks; } + void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } }; RadioPrefs radio; diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index ed198c61..275449e6 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -1,6 +1,7 @@ #include "CommonRadioPrefs.h" #include "TxtDataHelpers.h" #include "Utils.h" +#include "target.h" bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { if (strcmp(command, "get radio") == 0) { @@ -24,13 +25,28 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest setCodingRate(cr); setFreq(freq); setBandwidth(bw); - // NOTE: savePrefs() should be handled by caller strcpy(reply, "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } return true; } + + if (strcmp(command, "get freq") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getFreq())); + return true; + } + + if (strcmp(command, "get af") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor())); + return true; + } + if (memcmp(command, "set af ", 7) == 0) { + setAirtimeFactor(atof(&command[7])); + strcpy(reply, "OK"); + return true; + } + if (strcmp(command, "get dutycycle") == 0) { float dc = 100.0f / (getAirtimeFactor() + 1.0f); int dc_int = (int)dc; @@ -44,7 +60,6 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { setAirtimeFactor((100.0f / dc) - 1.0f); - // NOTE: savePrefs() should be handled by caller float actual = 100.0f / (getAirtimeFactor() + 1.0f); int a_int = (int)actual; int a_frac = (int)((actual - a_int) * 10.0f + 0.5f); @@ -52,5 +67,133 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest } return true; } + + if (strcmp(command, "get int.thresh") == 0) { + sprintf(reply, "> %d", (uint32_t) getIntThresh()); + return true; + } + if (memcmp(command, "set int.thresh ", 15) == 0) { + setIntThresh(atoi(&command[15])); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get cad") == 0) { + sprintf(reply, "> %s", isCadEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set cad ", 8) == 0) { + setCadEnabled(memcmp(&command[8], "on", 2) == 0); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get radio.rxgain") == 0) { + sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off"); + return true; + } + if (memcmp(command, "set radio.rxgain ", 17) == 0) { + bool enabled = memcmp(&command[17], "on", 2) == 0; + setRxGain(enabled); + if (radio_driver.setRxBoostedGainMode(enabled)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: unsupported"); + } + return true; + } + + if (memcmp(command, "get tx", 6) == 0 && (command[6] == 0 || command[6] == ' ')) { + sprintf(reply, "> %d", (int32_t) getTxPower()); + return true; + } + if (memcmp(command, "set tx ", 7) == 0) { + setTxPower(atoi(&command[7])); + radio_driver.setTxPower(getTxPower()); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get rxdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay())); + return true; + } + if (memcmp(command, "set rxdelay ", 12) == 0) { + float db = atof(&command[12]); + if (db >= 0 && db <= 20.0f) { + setRxDelay(db); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-20"); + } + return true; + } + + if (strcmp(command, "get agc.reset.interval") == 0) { + sprintf(reply, "> %d", (uint32_t) getAgcResetInt()); + return true; + } + if (memcmp(command, "set agc.reset.interval ", 23) == 0) { + setAgcResetInt(atoi(&command[23])); + sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); + return true; + } + + if (strcmp(command, "get path.hash.mode") == 0) { + sprintf(reply, "> %d", (uint32_t)getHashMode()); + return true; + } + if (memcmp(command, "set path.hash.mode ", 19) == 0) { + const char* config = command + 19; + uint8_t mode = atoi(config); + if (mode < 3) { + setHashMode(mode); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } + return true; + } + + if (strcmp(command, "get multi.acks") == 0) { + sprintf(reply, "> %d", (uint32_t) getMultiAcks()); + return true; + } + if (memcmp(command, "set multi.acks ", 15) == 0) { + setMultiAcks(atoi(&command[15])); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get txdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay())); + return true; + } + if (memcmp(command, "set txdelay ", 12) == 0) { + float f = atof(&command[12]); + if (f >= 0 && f <= 2.0f) { + setFloodTxDelay(f); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-2"); + } + return true; + } + + if (strcmp(command, "get direct.txdelay") == 0) { + sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay())); + return true; + } + if (memcmp(command, "set direct.txdelay ", 19) == 0) { + float f = atof(&command[19]); + if (f >= 0 && f <= 2.0f) { + setDirectTxDelay(f); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-2"); + } + return true; + } + return false; // not handled } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index eaec2c0a..c2aefd98 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -24,22 +24,35 @@ public: virtual float getAirtimeFactor() const = 0; virtual void setAirtimeFactor(float af) = 0; - // //def("cad", _parent->cad_enabled); - // //def("int_thr", _parent->interference_threshold); - // def("rxgain", _parent->rx_boosted_gain); - // #if 0 - // // NOTE: these cannot be set (yet) so don't load/save until we can. - // // also, fem_rxgain WAS mapped to wrong JSON property previously - // def("fem_rxgain", _parent->radio_fem_rxgain); - // def("fem_txgain", _parent->radio_fem_txgain); - // #endif - // def("tx", _parent->tx_power_dbm); - // def("rxdelay", _parent->rx_delay_base); - // //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded - // //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded - // //def("agc_int", _parent->agc_reset_interval); - // def("hash_mode", _parent->path_hash_mode); - // def("multi_ack", _parent->multi_acks); + virtual bool isCadEnabled() const = 0; + virtual void setCadEnabled(bool en) = 0; + + virtual uint8_t getIntThresh() const = 0; + virtual void setIntThresh(uint8_t t) = 0; + + virtual uint8_t getRxGain() const = 0; + virtual void setRxGain(uint8_t g) = 0; + + virtual uint8_t getTxPower() const = 0; + virtual void setTxPower(uint8_t dbm) = 0; + + virtual float getRxDelay() const = 0; + virtual void setRxDelay(float d) = 0; + + virtual uint8_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint8_t secs) = 0; + + virtual uint8_t getHashMode() const = 0; + virtual void setHashMode(uint8_t m) = 0; + + virtual uint8_t getMultiAcks() const = 0; + virtual void setMultiAcks(uint8_t m) = 0; + + virtual float getFloodTxDelay() const = 0; + virtual void setFloodTxDelay(float d) = 0; + + virtual float getDirectTxDelay() const = 0; + virtual void setDirectTxDelay(float d) = 0; bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); }; From 21179760d37eb174b4bf824714d8f6380418a3b1 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 19:35:43 +1000 Subject: [PATCH 051/174] "Unknown command" replies --- examples/companion_radio/MyMesh.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 54be91c7..5f093f2b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -533,10 +533,10 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection - if (from.isRemoteCLIAllowed() && handleCommand(text, sender_timestamp, reply)) { - // CLI command was handled. Let BaseChatMesh handle the sending of the reply - } else { - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + if (from.isRemoteCLIAllowed()) { + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + } } } @@ -1095,14 +1095,13 @@ void MyMesh::handleCmdFrame(size_t len) { text[tlen] = 0; // ensure null reply_buf[0] = 0; - if (handleCommand(text, 0, reply_buf)) { - out_frame[0] = RESP_CODE_CLI_REPLY; - int rlen = strlen(reply_buf); - memcpy(&out_frame[1], reply_buf, rlen); - _serial->writeFrame(out_frame, 1 + rlen); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // unsupported command + if (!handleCommand(text, 0, reply_buf)) { + strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text' } + out_frame[0] = RESP_CODE_CLI_REPLY; + int rlen = strlen(reply_buf); + memcpy(&out_frame[1], reply_buf, rlen); + _serial->writeFrame(out_frame, 1 + rlen); } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { int i = 1; uint8_t txt_type = cmd_frame[i++]; From f7c568e3d9d060940fb74cbc13a8896289b1dbd5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 20:03:49 +1000 Subject: [PATCH 052/174] onCommandDataRecv() use '>' prefix for CLI replies. --- examples/companion_radio/MyMesh.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5f093f2b..b86af093 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -534,8 +534,13 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3 const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection if (from.isRemoteCLIAllowed()) { - if (!handleCommand(text, sender_timestamp, reply)) { - strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + if (text[0] == '>') { // is this a CLI reply? + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, &text[1]); + } else { + *reply++ = '>'; // ensure the special 'is reply' prefix + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' + } } } } From 52306bfe0f8b368d4bf47a0f7b2543d504226d7c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 20:07:28 +1000 Subject: [PATCH 053/174] legacy queueMessage() case, eg. repeater replies --- examples/companion_radio/MyMesh.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b86af093..489fd69c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -542,6 +542,8 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3 strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' } } + } else { + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); } } From 2c0ace2519d9c910b3b8fd1cba93f0a61e705d35 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 22:04:05 +1000 Subject: [PATCH 054/174] * new TXT_TYPE_CLI_COMMAND (3) --- examples/companion_radio/MyMesh.cpp | 18 +++++++++--------- examples/companion_radio/MyMesh.h | 2 ++ examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 4 ++-- examples/simple_secure_chat/main.cpp | 6 +++++- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/BaseChatMesh.cpp | 8 ++++++-- src/helpers/BaseChatMesh.h | 3 ++- src/helpers/TxtDataHelpers.h | 4 +++- 9 files changed, 31 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 489fd69c..99b3196d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -530,20 +530,20 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); } -void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, +void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) { + markConnectionActive(from); // in case this is from a server, and we have a connection + queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); +} + +void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) { markConnectionActive(from); // in case this is from a server, and we have a connection if (from.isRemoteCLIAllowed()) { - if (text[0] == '>') { // is this a CLI reply? - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, &text[1]); - } else { - *reply++ = '>'; // ensure the special 'is reply' prefix - if (!handleCommand(text, sender_timestamp, reply)) { - strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' - } + if (!handleCommand(text, sender_timestamp, reply)) { + strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text' } } else { - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); + queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index b60030a6..780de35d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -134,6 +134,8 @@ protected: void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) override; void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) override; + void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text, char* reply) override; void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a..9f3c90e9 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -704,7 +704,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks bool is_retry = (sender_timestamp == client->last_timestamp); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094f..b786241a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -443,7 +443,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries bool is_retry = (sender_timestamp == client->last_timestamp); @@ -463,7 +463,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint8_t temp[166]; bool send_ack; - if (flags == TXT_TYPE_CLI_DATA) { + if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { if (client->isAdmin()) { if (is_retry) { temp[5] = 0; // no reply diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 241fe1c2..159249df 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -240,8 +240,12 @@ protected: } } - void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { + void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { } + + void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override { + } + void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 9bfa5ec6..b4b42f64 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -576,7 +576,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i sendAckTo(*from, ack_hash, packet->getPathHashSize()); } } - } else if (flags == TXT_TYPE_CLI_DATA) { + } else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) { from->last_timestamp = sender_timestamp; from->last_activity = getRTCClock()->getCurrentTime(); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index bf8a861c..d66aabca 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -256,13 +256,17 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { + char *text = (char *)&data[5]; + onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know + // NOTE: no ack expected for CLI_DATA replies + } else if (flags == TXT_TYPE_CLI_COMMAND) { uint8_t temp[166]; char *command = (char *)&data[5]; char *reply = (char *)&temp[5]; *reply = 0; - onCommandDataRecv(from, packet, sender_timestamp, command, reply); // let UI know - // NOTE: no ack expected for CLI_DATA replies + onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know + // NOTE: no ack expected for CLI_COMMAND replies int text_len = strlen(reply); if (text_len > 0) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 331d1041..a5fe54d5 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -113,7 +113,8 @@ protected: virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len); virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; - virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; + virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; + virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0; virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0; virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0; virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index ece494f2..693a47ca 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -4,8 +4,10 @@ #include #define TXT_TYPE_PLAIN 0 // a plain text message -#define TXT_TYPE_CLI_DATA 1 // a CLI command +#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply #define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender +#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly) + #define DATA_TYPE_RESERVED 0x0000 // reserved for future use #define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps From e485d01dcbb18485ae79cc6622509f3a8f9dbdc8 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 23 Aug 2026 22:49:45 +1000 Subject: [PATCH 055/174] support for CMD_SEND_TXT_MSG and new TXT_TYPE_CLI_COMMAND --- examples/companion_radio/MyMesh.cpp | 6 +++--- src/helpers/BaseChatMesh.cpp | 4 ++-- src/helpers/BaseChatMesh.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 99b3196d..b19c8d7f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1119,16 +1119,16 @@ void MyMesh::handleCmdFrame(size_t len) { uint8_t *pub_key_prefix = &cmd_frame[i]; i += 6; ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); - if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) { char *text = (char *)&cmd_frame[i]; int tlen = len - i; uint32_t est_timeout; text[tlen] = 0; // ensure null int result; uint32_t expected_ack; - if (txt_type == TXT_TYPE_CLI_DATA) { + if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) { msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); + result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout); expected_ack = 0; // no Ack expected } else { result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index d66aabca..28592415 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -489,13 +489,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, return rc; } -int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { +int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); + temp[4] = (attempt & 3) | (txt_type << 2); memcpy(&temp[5], text, text_len + 1); auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index a5fe54d5..0a8fdef4 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -157,7 +157,7 @@ public: mesh::Packet* createSelfAdvert(const char* name); mesh::Packet* createSelfAdvert(const char* name, double lat, double lon); int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout); - int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); + int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); From 088ae3caebceb79b7df02e86475ca874eb122ec0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 23 Aug 2026 10:42:00 -0400 Subject: [PATCH 056/174] ui: opt-out for discover screen --- examples/companion_radio/MyMesh.cpp | 4 ++++ examples/companion_radio/MyMesh.h | 12 +++++++++++- examples/companion_radio/ui-new/UITask.cpp | 11 ++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01512b16..75566448 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -407,6 +407,7 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; if (max_num > disc_nodes_count) max_num = disc_nodes_count; @@ -451,6 +452,7 @@ void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len } disc_nodes_count ++; } +#endif void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; @@ -832,7 +834,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); +#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 848d21c0..25b56930 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -84,6 +84,7 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) struct DiscoveredNode { uint8_t pubkey_prefix[9]; float snr_in; @@ -91,6 +92,7 @@ struct DiscoveredNode { char name[32]; uint8_t type; }; +#endif class MyMesh : public BaseChatMesh, public DataStoreHost { public: @@ -110,8 +112,10 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) bool requestRepeatersDiscovery(); int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); +#endif protected: float getAirtimeBudgetFactor() const override; @@ -268,12 +272,18 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - #define DISCOVERED_NODES_TABLE_SIZE 10 +#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept uint32_t disc_node_req_tag = 0; uint32_t disc_nodes_count = 0; void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); +#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 35525028..0193ebbc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -103,7 +103,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DISCOVERY, +#endif SHUTDOWN, Count // keep as last }; @@ -115,9 +117,11 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) +#endif void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -463,6 +467,7 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) } else if (_page == HomePage::DISCOVERY) { int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); display.setColor(UIColor::primary_txt); @@ -495,7 +500,7 @@ public: y = 10 + 11 * UI_RECENT_LIST_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } - +#endif } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -521,9 +526,11 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } +#endif return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { @@ -556,6 +563,7 @@ public: return true; } #endif +#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter the_mesh.requestRepeatersDiscovery(); @@ -567,6 +575,7 @@ public: discovery_disp_names = !discovery_disp_names; return true; } +#endif if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; From 47b0b7b3b8a501614d9f935eb96d794170afb5c5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 01:50:22 +1000 Subject: [PATCH 057/174] * T096, and Station G3: refactoring the 'FEM' prefs to variant-specific code * CommonCLI: 'FEM' commands removed * introduced static no-op attachDynamicPrefs() for all boards --- examples/companion_radio/MyMesh.cpp | 11 ++- examples/companion_radio/NodePrefs.h | 13 +-- examples/simple_repeater/MyMesh.cpp | 4 +- examples/simple_room_server/MyMesh.cpp | 4 +- examples/simple_sensor/SensorMesh.cpp | 4 +- src/MeshCore.h | 9 +- src/helpers/CommonCLI.cpp | 61 ++----------- src/helpers/CommonCLI.h | 7 +- src/helpers/CommonRadioPrefs.cpp | 21 +++++ src/helpers/CommonRadioPrefs.h | 13 ++- src/helpers/ConfigSerializer.h | 6 ++ src/helpers/ESP32Board.h | 3 + src/helpers/KeyValueStore.h | 11 +++ src/helpers/NRF52Board.h | 3 + src/helpers/stm32/STM32Board.h | 3 + variants/heltec_t096/T096Board.cpp | 48 +++++++++- variants/heltec_t096/T096Board.h | 12 ++- variants/station_g3_esp32/StationG3Board.cpp | 88 +++++++++++++++++-- variants/station_g3_esp32/StationG3Board.h | 17 ++-- .../waveshare_rp2040_lora/WaveshareBoard.h | 3 + variants/xiao_rp2040/XiaoRP2040Board.h | 3 + 21 files changed, 243 insertions(+), 101 deletions(-) create mode 100644 src/helpers/KeyValueStore.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b19c8d7f..b8068dd9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -989,8 +989,9 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); + MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -2073,6 +2074,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + // hook for variant-specific CLI processing + if (board.handleCommand(command, sender_timestamp, reply)) { + if (_prefs.isDirty()) { savePrefs(); } + return true; + } + if (memcmp(command, "set name ", 9) == 0) { if (AdvertDataParser::isValidName(&command[9])) { StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f1cb69cc..68af0911 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -54,12 +54,8 @@ private: //def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); - #if 0 - // NOTE: these cannot be set (yet) so don't load/save until we can. - // also, fem_rxgain WAS mapped to wrong JSON property previously - def("fem_rxgain", _parent->radio_fem_rxgain); + def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously def("fem_txgain", _parent->radio_fem_txgain); - #endif def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); @@ -103,6 +99,10 @@ private: void setFloodTxDelay(float d) override { /* no-op */ } float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded void setDirectTxDelay(float d) override { /* no-op */ } + uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } + void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } + uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } + void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; @@ -177,5 +177,6 @@ public: void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } CommonRadioPrefs* getRadioPrefs() { return &radio; } - void clearDirty() { radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9f3c90e9..d7371317 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -982,8 +982,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index b786241a..33a67bde 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -725,8 +725,8 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b4b42f64..97b39dac 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -771,8 +771,8 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); - board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + + board.attachDynamicPrefs(_prefs.getRadioPrefs()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef..43495232 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,13 +64,6 @@ public: virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported - virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemLna() const { return false; } - virtual bool isLoRaFemLnaEnabled() const { return false; } - // Software-selectable external FEM transmit gain. This is not a PA power switch. - virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; } - virtual bool canControlLoRaFemPaGain() const { return false; } - virtual bool isLoRaFemPaGainEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } @@ -79,6 +72,8 @@ public: virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2e9efa9c..4930e81e 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -185,6 +185,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); } return; } + // hook for variant-specific CLI processing + if (_board->handleCommand(command, sender_timestamp, reply)) { + if (_prefs->isDirty()) { savePrefs(); } + return; + } if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return @@ -507,50 +512,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); - } else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemLnaEnabled(true)) { - _prefs->radio_fem_rxgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemLnaEnabled(false)) { - _prefs->radio_fem_rxgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM RX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } - } else if (memcmp(config, "radio.fem.txgain ", 17) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else if (memcmp(&config[17], "on", 2) == 0) { - if (_board->setLoRaFemPaGainEnabled(true)) { - _prefs->radio_fem_txgain = 1; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain on"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else if (memcmp(&config[17], "off", 3) == 0) { - if (_board->setLoRaFemPaGainEnabled(false)) { - _prefs->radio_fem_txgain = 0; - savePrefs(); - strcpy(reply, "OK - LoRa FEM TX gain off"); - } else { - strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); - } - } else { - strcpy(reply, "Error: state must be on or off"); - } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); savePrefs(); @@ -733,18 +694,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); - } else if (memcmp(config, "radio.fem.rxgain", 16) == 0) { - if (!_board->canControlLoRaFemLna()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); - } - } else if (memcmp(config, "radio.fem.txgain", 16) == 0) { - if (!_board->canControlLoRaFemPaGain()) { - strcpy(reply, "Error: unsupported"); - } else { - sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); - } } else if (memcmp(config, "flood.max.advert", 16) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 69243f52..5735abce 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -128,6 +128,10 @@ private: void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } + uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } + void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } + uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } + void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); } }; RadioPrefs radio; @@ -226,7 +230,8 @@ public: } CommonRadioPrefs* getRadioPrefs() { return &radio; } - void clearDirty() { radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 275449e6..60d52e15 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -3,6 +3,27 @@ #include "Utils.h" #include "target.h" +void CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { + if (strcmp(key, "fem_rxgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); + } else if (strcmp(key, "fem_txgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); + } else { + MESH_DEBUG_PRINTLN("Error: getBykey() unknown key: %s", key); + } +} +void CommonRadioPrefs::setByKey(const char* key, const char* value) { + if (strcmp(key, "fem_rxgain") == 0) { + setFEMRxGain(atoi(value)); + markDirty(); + } else if (strcmp(key, "fem_txgain") == 0) { + setFEMTxGain(atoi(value)); + markDirty(); + } else { + MESH_DEBUG_PRINTLN("Error: setBykey() unknown key: %s", key); + } +} + bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { if (strcmp(command, "get radio") == 0) { char freq[16], bw[16]; diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index c2aefd98..de3e515f 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -1,6 +1,8 @@ +#pragma once #include "ConfigSerializer.h" +#include "KeyValueStore.h" -class CommonRadioPrefs : public ConfigSerializer { +class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore{ bool _is_dirty = false; protected: CommonRadioPrefs() { } @@ -54,5 +56,14 @@ public: virtual float getDirectTxDelay() const = 0; virtual void setDirectTxDelay(float d) = 0; + virtual uint8_t getFEMRxGain() const = 0; + virtual void setFEMRxGain(uint8_t g) = 0; + + virtual uint8_t getFEMTxGain() const = 0; + virtual void setFEMTxGain(uint8_t g) = 0; + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); + + void setByKey(const char* key, const char* value) override; // for dynamic key/value access + void getByKey(const char* key, char* value, size_t max_len) override; }; diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 7e6d6f2a..47ab5e81 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -17,6 +17,7 @@ class ConfigSerializer { bool _first; int8_t _depth; + bool _dirty = false; enum OP { READ, WRITE }; @@ -62,7 +63,12 @@ protected: virtual void structure() = 0; + void markDirty() { _dirty = true; } + public: bool loadSerial(Stream& s); bool saveSerial(Stream& s); + + virtual bool isDirty() const { return _dirty; } + virtual void clearDirty() { _dirty = false; } }; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index d7eb5fee..75428bc6 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -15,6 +15,7 @@ #include "soc/rtc.h" #include "esp_system.h" #include +#include class ESP32Board : public mesh::MainBoard { protected: @@ -51,6 +52,8 @@ public: #endif } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + // Temperature from ESP32 MCU float getMCUTemperature() override { uint32_t raw = 0; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h new file mode 100644 index 00000000..4bebb4d2 --- /dev/null +++ b/src/helpers/KeyValueStore.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +class KeyValueStore { +protected: + KeyValueStore() { } +public: + virtual void setByKey(const char* key, const char* value) { } + virtual void getByKey(const char* key, char* value, size_t max_len) { } +}; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f97..4dbfe1ca 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(NRF52_PLATFORM) @@ -57,6 +58,8 @@ public: virtual void sleep(uint32_t secs) override; bool isExternalPowered() override; + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } virtual uint32_t getResetReason() const override { return reset_reason; } diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h index 06bc768f..016d1fb1 100644 --- a/src/helpers/stm32/STM32Board.h +++ b/src/helpers/stm32/STM32Board.h @@ -2,6 +2,7 @@ #include #include +#include class STM32Board : public mesh::MainBoard { protected: @@ -12,6 +13,8 @@ public: startup_reason = BD_STARTUP_NORMAL; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + uint8_t getStartupReason() const override { return startup_reason; } uint16_t getBattMilliVolts() override { diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 78af529d..255a703d 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool T096Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); -} - bool T096Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void T096Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("radio.fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("radio.fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("radio.fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 15c7e68b..1fa7ee28 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -4,28 +4,34 @@ #include #include #include +#include #include "LoRaFEMControl.h" class T096Board : public NRF52BoardDCDC { + KeyValueStore* _prefs = NULL; + protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif void variant_shutdown(); + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + public: RefCountedDigitalPin periph_power; LoRaFEMControl loRaFEMControl; T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {} void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; }; diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp index dd863aca..277c2b8b 100644 --- a/variants/station_g3_esp32/StationG3Board.cpp +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -21,10 +21,6 @@ bool StationG3Board::setLoRaFemLnaEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemLna() const { - return loRaFEMControl.canControlLNA(); -} - bool StationG3Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } @@ -37,10 +33,86 @@ bool StationG3Board::setLoRaFemPaGainEnabled(bool enable) { return true; } -bool StationG3Board::canControlLoRaFemPaGain() const { - return loRaFEMControl.canControlPAGain(); -} - bool StationG3Board::isLoRaFemPaGainEnabled() const { return loRaFEMControl.isPAGainEnabled(); } + +void StationG3Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_rxgain", gain, 7); // get initial values + setLoRaFemLnaEnabled(strcmp(gain, "1") == 0); + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool StationG3Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.canControlLNA()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.canControlLNA()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (!loRaFEMControl.canControlPAGain()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + strcpy(reply, "OK - LoRa FEM TX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index 52628eb6..b4c5838c 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -6,6 +6,12 @@ #include "LoRaFEMControl.h" class StationG3Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const; public: LoRaFEMControl loRaFEMControl; @@ -25,6 +31,10 @@ public: } } + void attachDynamicPrefs(KeyValueStore* prefs); + + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void setPrimaryLNAEnable(bool enabled) { loRaFEMControl.setLNAEnable(enabled); } @@ -43,13 +53,6 @@ public: loRaFEMControl.setRxModeEnable(); } - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; - bool setLoRaFemPaGainEnabled(bool enable) override; - bool canControlLoRaFemPaGain() const override; - bool isLoRaFemPaGainEnabled() const override; - void powerOff() override; uint16_t getBattMilliVolts() override { diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index 694b8bd1..e7c70a5d 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -2,6 +2,7 @@ #include #include +#include // LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF // https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf @@ -32,6 +33,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index d2951c75..cad399de 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -2,6 +2,7 @@ #include #include +#include /* * This board has no built-in way to read battery voltage. @@ -30,6 +31,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + #ifdef P_LORA_TX_LED void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } From a1cf5bd806e27e25b3285e34c2a6b94fab0e5194 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 02:16:18 +1000 Subject: [PATCH 058/174] * refactored 'FEM' commands for HeltecTrackerV2 & HeltecV4 --- variants/heltec_t096/T096Board.cpp | 6 +-- variants/heltec_t096/T096Board.h | 3 +- .../HeltecTrackerV2Board.cpp | 48 +++++++++++++++++-- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 10 ++-- variants/heltec_v4/HeltecV4Board.cpp | 48 +++++++++++++++++-- variants/heltec_v4/HeltecV4Board.h | 10 ++-- 6 files changed, 106 insertions(+), 19 deletions(-) diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 255a703d..ea26d2cc 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -139,7 +139,7 @@ void T096Board::attachDynamicPrefs(KeyValueStore* prefs) { _prefs = prefs; char radio_fem_rxgain[8] = { 0 }; - _prefs->getByKey("radio.fem_rxgain", radio_fem_rxgain, 7); // get initial values + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); } @@ -158,14 +158,14 @@ bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, ch strcpy(reply, "Error: unsupported"); } else if (memcmp(&command[21], "on", 2) == 0) { if (setLoRaFemLnaEnabled(true)) { - _prefs->setByKey("radio.fem_rxgain", "1"); + _prefs->setByKey("fem_rxgain", "1"); strcpy(reply, "OK - LoRa FEM RX gain on"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); } } else if (memcmp(&command[21], "off", 3) == 0) { if (setLoRaFemLnaEnabled(false)) { - _prefs->setByKey("radio.fem_rxgain", "0"); + _prefs->setByKey("fem_rxgain", "0"); strcpy(reply, "OK - LoRa FEM RX gain off"); } else { strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 1fa7ee28..e926c8a6 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -26,12 +26,11 @@ public: T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {} void begin(); void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; void powerOff() override; - - bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; }; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 99b1cdfe..753824a0 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -72,10 +72,50 @@ void HeltecTrackerV2Board::begin() { return true; } - bool HeltecTrackerV2Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a025..5fed2b65 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -6,6 +6,10 @@ #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; public: RefCountedDigitalPin periph_power; @@ -14,13 +18,13 @@ public: HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; }; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3f13f41f..794257d7 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -73,10 +73,50 @@ void HeltecV4Board::begin() { return true; } - bool HeltecV4Board::canControlLoRaFemLna() const { - return loRaFEMControl.isLnaCanControl(); - } - bool HeltecV4Board::isLoRaFemLnaEnabled() const { return loRaFEMControl.isLNAEnabled(); } + +void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb3..79a690e8 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -10,6 +10,10 @@ #endif class HeltecV4Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -20,12 +24,12 @@ public: HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void powerOff() override; - bool setLoRaFemLnaEnabled(bool enable) override; - bool canControlLoRaFemLna() const override; - bool isLoRaFemLnaEnabled() const override; uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { if (multiplier == 0.0f) { From acac4af3ec3104d8c15c3dd8eb5f0aec75854260 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 23 Aug 2026 20:13:12 +0200 Subject: [PATCH 059/174] FAQ: Fixed instructions on wiping and resetting nrf52 devices --- docs/faq.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index c7d7a118..e53d3a4d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -691,23 +691,28 @@ You can get the epoch time on and use it to set ### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. **A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. -### 6.7. Q: My RAK/T1000-E/xiao_nRF52 device seems to be corrupted, how do I wipe it clean to start fresh? -**A:** +### 6.7. Q: My RAK/T1000-E/xiao_nRF52/... device seems to be corrupted, how do I wipe it clean to start fresh? +**A: If you're able to connect to the device from your MeshCore app** +1. Navigate to Gear icon(Settings) on the upper right corner +2. Click on `Export Settings` button, choose `Select All` and Confirm. This will save your Node configuration +3. Now choose `Factory Reset` option +4. Confirm that you want to reset your device +5. Go to Bluetooth System settings and remove pairing of the device, so you can connect again +6. Connect your device in the app and enter the PIN +7. Navigate to Gear icon(Settings) on the upper right corner +8. Click on `Import Settings` and choose the file from 2., click on `Select All` and Confirm +9. Find `Reboot` button on of the Settings list -1. Connect USB-C cable to your device, per your device's instruction, get it to flash mode: - - For RAK, click the reset button **TWICE** - - For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE** - - For Heltec T114, click the reset button **TWICE** (the bottom button) - - For Xiao nRF52, click the reset button once. If that doesn't work, quickly double-click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader)) -2. A new folder will appear on your computer's desktop -3. Download the `flash_erase*.uf2` file for your device on - - RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2` - - Seeed Studio Xiao nRF52 WIO: `Flash_erase-nRF52_softdevice_v7.uf2` -4. drag and drop the uf2 file for your device to the root of the new folder -5. Wait for the copy to complete. You might get an error dialog, you can ignore it -6. Go to , click `Console` and select the serial port for your connected device -7. In the console, press enter. Your flash should now be erased -8. You may now flash the latest MeshCore firmware onto your device +**B: You're not able to connect to the App** +1. Connect USB cable to your device +2. Go to https://flasher.meshcore.io +3. Search for your device in the list +4. Choose `Companion Bluetooth` +5. Press `Enter DFU mode` button, choose your USB device +6. Press `Erase Flash` button, choose the device again and wait until it completes +7. Press `Flash!` button and choose the USB device last time +8. The device is erased and newest firmware is installed +9. You might need to remove the pairing in Bluetooth System Settings in order to re-pair the app again. Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If your device has a user button (e.g. some RAK, T114), you can activate the rescue mode by holding down the user button of the device within 8 seconds of boot. Then you can use the 'Console' on @@ -716,9 +721,9 @@ Separately, starting in firmware version 1.7.0, there is a CLI Rescue mode. If y `NetworkError: Failed to execute 'open' on 'SerialPort': Failed to open serial port.` -Allow the browser user on it: +Allow user access on your USB port: -`# setfacl -m u:YOUR_USER_HERE:rw /dev/ttyUSB0` +`sudo setfacl -m u:$USER:rw /dev/ttyUSB0` --- From 7dc2d54818809a44d04d7d3132ea9a416faa331b Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 24 Aug 2026 13:45:19 +1200 Subject: [PATCH 060/174] add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tracker l1 --- examples/companion_radio/ui-new/UITask.cpp | 6 ++++++ variants/wio-tracker-l1/platformio.ini | 2 ++ 2 files changed, 8 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 55755ef1..969ac3dd 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -102,7 +102,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -459,6 +461,7 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#ifndef UI_NO_HIBERNATE } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -470,6 +473,7 @@ public: display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } +#endif } return 5000; // next render after 5000 ms } @@ -516,10 +520,12 @@ public: return true; } #endif +#ifndef UI_NO_HIBERNATE if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; } +#endif return false; } }; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2..19139c28 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -95,6 +96,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"' From 5a162ff4c640c6b033912ee8478590705be16018 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 17:12:56 +1000 Subject: [PATCH 061/174] * new DynamicConfigSerializer * board/variant KeyValueStore now can write to 'custom' object in Json prefs --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/NodePrefs.h | 8 ++- examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 2 +- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/CommonCLI.h | 8 ++- src/helpers/CommonRadioPrefs.cpp | 22 ++++--- src/helpers/CommonRadioPrefs.h | 6 +- src/helpers/DynamicConfigSerializer.cpp | 83 +++++++++++++++++++++++++ src/helpers/DynamicConfigSerializer.h | 20 ++++++ src/helpers/KeyValueStore.h | 4 +- 11 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 src/helpers/DynamicConfigSerializer.cpp create mode 100644 src/helpers/DynamicConfigSerializer.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b8068dd9..011df278 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -990,7 +990,7 @@ void MyMesh::begin(bool has_display) { radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 68af0911..d2a011bf 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -2,6 +2,7 @@ #include // For uint8_t, uint32_t #include #include +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -154,6 +155,8 @@ private: }; CompanionPrefs companion; + DynamicConfigSerializer custom; + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -165,9 +168,10 @@ protected: def("gps", gps); def("repeat", repeat); def("comp", companion); + def("custom", custom); } public: - NodePrefs() : radio(this), gps(this), companion(this) { + NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) { node_name[0] = 0; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); @@ -177,6 +181,8 @@ public: void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } CommonRadioPrefs* getRadioPrefs() { return &radio; } + KeyValueStore* getCustom() { return &custom; } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d7371317..ca6a3e60 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -983,7 +983,7 @@ void MyMesh::begin(FILESYSTEM *fs) { MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 33a67bde..71b8d32a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -726,7 +726,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 97b39dac..23d0cdc3 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -772,7 +772,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); - board.attachDynamicPrefs(_prefs.getRadioPrefs()); + board.attachDynamicPrefs(_prefs.getCustom()); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 5735abce..3fb03dc5 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -7,6 +7,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -202,6 +203,8 @@ private: }; RoomPrefs room; + DynamicConfigSerializer custom; + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -218,10 +221,11 @@ protected: def("repeat", repeat); def("room", room); def("power", power); + def("custom", custom); } public: - NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this), custom(&radio) { node_name[0] = 0; password[0] = 0; guest_password[0] = 0; @@ -230,6 +234,8 @@ public: } CommonRadioPrefs* getRadioPrefs() { return &radio; } + KeyValueStore* getCustom() { return &custom; } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } }; diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 60d52e15..a25df880 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -3,25 +3,29 @@ #include "Utils.h" #include "target.h" -void CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { +bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { if (strcmp(key, "fem_rxgain") == 0) { snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); - } else if (strcmp(key, "fem_txgain") == 0) { - snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); - } else { - MESH_DEBUG_PRINTLN("Error: getBykey() unknown key: %s", key); + return true; } + if (strcmp(key, "fem_txgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); + return true; + } + return false; } -void CommonRadioPrefs::setByKey(const char* key, const char* value) { +bool CommonRadioPrefs::setByKey(const char* key, const char* value) { if (strcmp(key, "fem_rxgain") == 0) { setFEMRxGain(atoi(value)); markDirty(); - } else if (strcmp(key, "fem_txgain") == 0) { + return true; + } + if (strcmp(key, "fem_txgain") == 0) { setFEMTxGain(atoi(value)); markDirty(); - } else { - MESH_DEBUG_PRINTLN("Error: setBykey() unknown key: %s", key); + return true; } + return false; } bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index de3e515f..96895bb2 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -2,7 +2,7 @@ #include "ConfigSerializer.h" #include "KeyValueStore.h" -class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore{ +class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore { bool _is_dirty = false; protected: CommonRadioPrefs() { } @@ -64,6 +64,6 @@ public: bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply); - void setByKey(const char* key, const char* value) override; // for dynamic key/value access - void getByKey(const char* key, char* value, size_t max_len) override; + bool setByKey(const char* key, const char* value) override; // for dynamic key/value access + bool getByKey(const char* key, char* value, size_t max_len) override; }; diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp new file mode 100644 index 00000000..7ec6dc4e --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -0,0 +1,83 @@ +#include "DynamicConfigSerializer.h" +#include + +#define PROP_SEP_CHAR '|' +#define PROP_SEP_STR "|" +#define KEY_SEP_CHAR ':' +#define KEY_SEP_STR ":" + +bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { + if (_fallback && _fallback->setByKey(key, value)) return true; + + // TODO: guard for bad chars (':' or '|') + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + // add/replace in _config[] + char new_config[MAX_DYNAMIC_CONFG]; + new_config[0] = 0; + + int keylen = strlen(key); + for (int i = 0; i < n; i++) { + const char* item = parts[i]; + if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) { + // key exists, so omit old value from this pass (will append new value at end) + } else { + if (new_config[0]) { + strcat(new_config, PROP_SEP_STR); + } + strcat(new_config, item); + } + } + // now append new key/value (if it fits) + if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) { + strcat(new_config, key); + strcat(new_config, KEY_SEP_STR); + strcat(new_config, value); + strcpy(_config, new_config); // commit new serialized string + return true; + } + return false; // didn't fit in _config[] +} + +bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) { + if (_fallback && _fallback->getByKey(key, value, max_len)) return true; + + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + int keylen = strlen(key); + for (int i = 0; i < n; i++) { + const char* item = parts[i]; + if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) { + strncpy(value, &item[keylen+1], max_len); + value[max_len] = 0; + return true; + } + } + return false; +} + +void DynamicConfigSerializer::structure() { + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy + + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + + // dynamically call def()'s + for (int i = 0; i < n; i++) { + char* item = (char *) parts[i]; + char* eq = strchr(item, KEY_SEP_CHAR); + if (eq) { + *eq = 0; // replace separator with null terminator + def(item, eq + 1, MAX_DYNAMIC_CONFG/2); // maximum HALF of total for individual property value + } + } +} diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h new file mode 100644 index 00000000..102f8e97 --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.h @@ -0,0 +1,20 @@ +#include "ConfigSerializer.h" +#include "KeyValueStore.h" + +#ifndef MAX_DYNAMIC_CONFG + #define MAX_DYNAMIC_CONFG 128 +#endif + +class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { + char _config[MAX_DYNAMIC_CONFG]; + KeyValueStore* _fallback; + +protected: + void structure() override; + +public: + DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { _config[0] = 0; } + + bool setByKey(const char* key, const char* value) override; + bool getByKey(const char* key, char* value, size_t max_len) override; +}; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h index 4bebb4d2..cc4ac455 100644 --- a/src/helpers/KeyValueStore.h +++ b/src/helpers/KeyValueStore.h @@ -6,6 +6,6 @@ class KeyValueStore { protected: KeyValueStore() { } public: - virtual void setByKey(const char* key, const char* value) { } - virtual void getByKey(const char* key, char* value, size_t max_len) { } + virtual bool setByKey(const char* key, const char* value) { return false; } + virtual bool getByKey(const char* key, char* value, size_t max_len) { return false; } }; From 845242f4aabf8735913cbe5db7a6848c2206d240 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 17:22:15 +1000 Subject: [PATCH 062/174] * prefs, custom dirty state --- examples/companion_radio/NodePrefs.h | 4 ++-- src/helpers/CommonCLI.h | 4 ++-- src/helpers/DynamicConfigSerializer.cpp | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d2a011bf..c79e2f0f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -183,6 +183,6 @@ public: CommonRadioPrefs* getRadioPrefs() { return &radio; } KeyValueStore* getCustom() { return &custom; } - bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } - void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } }; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3fb03dc5..17b3da87 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -236,8 +236,8 @@ public: CommonRadioPrefs* getRadioPrefs() { return &radio; } KeyValueStore* getCustom() { return &custom; } - bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty(); } - void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); } + bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); } + void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); } }; class CommonCLICallbacks { diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp index 7ec6dc4e..dc08d7d7 100644 --- a/src/helpers/DynamicConfigSerializer.cpp +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -38,6 +38,7 @@ bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { strcat(new_config, KEY_SEP_STR); strcat(new_config, value); strcpy(_config, new_config); // commit new serialized string + markDirty(); return true; } return false; // didn't fit in _config[] From 8ccc9928a83b4035f40ccc7ad99a2fd2de03c093 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 19:42:55 +1000 Subject: [PATCH 063/174] * DynamicConfigSerializer fixes, and unit tests --- platformio.ini | 1 + src/helpers/CommonCLI.h | 4 +- src/helpers/ConfigSerializer.h | 4 +- src/helpers/DynamicConfigSerializer.cpp | 39 ++++--- src/helpers/DynamicConfigSerializer.h | 3 + .../test_config_serializer.cpp | 105 +++++++++++++----- 6 files changed, 115 insertions(+), 41 deletions(-) diff --git a/platformio.ini b/platformio.ini index ee502473..2219c978 100644 --- a/platformio.ini +++ b/platformio.ini @@ -171,6 +171,7 @@ build_src_filter = +<../src/Utils.cpp> +<../src/Packet.cpp> +<../src/helpers/ConfigSerializer.cpp> + +<../src/helpers/DynamicConfigSerializer.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 17b3da87..8591cdc1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -104,13 +104,13 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } - void setCadEnabled(bool en) override { _parent->cad_enabled; markDirty(); } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } uint8_t getIntThresh() const override { return _parent->interference_threshold; } void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 47ab5e81..e55b1b12 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -19,6 +19,7 @@ class ConfigSerializer { int8_t _depth; bool _dirty = false; +protected: enum OP { READ, WRITE }; class Context { @@ -39,13 +40,14 @@ class ConfigSerializer { const char* getToken() const { return rd_buf; } bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } + const char* getKey(uint8_t depth) { return _keys[depth]; } }; Context* _context = NULL; + int8_t getDepth() const { return _depth; } void writeComma(); -protected: ConfigSerializer() { } void def(const char* key, char* value, size_t max_len); // max_len inclusive of null diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp index dc08d7d7..b9c6c267 100644 --- a/src/helpers/DynamicConfigSerializer.cpp +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -6,7 +6,7 @@ #define KEY_SEP_CHAR ':' #define KEY_SEP_STR ":" -bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { +bool DynamicConfigSerializer::setByKeyPrv(const char* key, const char* value) { if (_fallback && _fallback->setByKey(key, value)) return true; // TODO: guard for bad chars (':' or '|') @@ -34,16 +34,26 @@ bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { } // now append new key/value (if it fits) if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) { + if (new_config[0]) { + strcat(new_config, PROP_SEP_STR); + } strcat(new_config, key); strcat(new_config, KEY_SEP_STR); strcat(new_config, value); strcpy(_config, new_config); // commit new serialized string - markDirty(); return true; } return false; // didn't fit in _config[] } +bool DynamicConfigSerializer::setByKey(const char* key, const char* value) { + if (setByKeyPrv(key, value)) { + markDirty(); + return true; + } + return false; +} + bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) { if (_fallback && _fallback->getByKey(key, value, max_len)) return true; @@ -66,19 +76,22 @@ bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_ } void DynamicConfigSerializer::structure() { - char tmp[MAX_DYNAMIC_CONFG]; - strcpy(tmp, _config); // make a (modifiable) copy + if (_context->op() == OP::WRITE) { + char tmp[MAX_DYNAMIC_CONFG]; + strcpy(tmp, _config); // make a (modifiable) copy - const char* parts[8]; - int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); + const char* parts[8]; + int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR); - // dynamically call def()'s - for (int i = 0; i < n; i++) { - char* item = (char *) parts[i]; - char* eq = strchr(item, KEY_SEP_CHAR); - if (eq) { - *eq = 0; // replace separator with null terminator - def(item, eq + 1, MAX_DYNAMIC_CONFG/2); // maximum HALF of total for individual property value + for (int i = 0; i < n; i++) { + char* item = (char *) parts[i]; + char* eq = strchr(item, KEY_SEP_CHAR); + if (eq) { + *eq = 0; // replace separator with null terminator + def(item, eq + 1, MAX_DYNAMIC_CONFG/2); + } } + } else { + setByKeyPrv(_context->getKey(getDepth()), _context->getToken()); } } diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h index 102f8e97..e5dc4c7c 100644 --- a/src/helpers/DynamicConfigSerializer.h +++ b/src/helpers/DynamicConfigSerializer.h @@ -1,3 +1,4 @@ +#pragma once #include "ConfigSerializer.h" #include "KeyValueStore.h" @@ -9,6 +10,8 @@ class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { char _config[MAX_DYNAMIC_CONFG]; KeyValueStore* _fallback; + bool setByKeyPrv(const char* key, const char* value); + protected: void structure() override; diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index 27c3c811..dec55483 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -1,13 +1,6 @@ #include #include "helpers/ConfigSerializer.h" - -class NativeFileSystem { -public: - void mkdir(const char*) { } -}; -#define FILESYSTEM NativeFileSystem -#include "helpers/CommonCLI.h" -#undef FILESYSTEM +#include "helpers/DynamicConfigSerializer.h" #define TEST_INT_S "56" #define TEST_INT 56 @@ -192,28 +185,90 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } -TEST(NodePrefs, FemGainSettingsRoundTrip) { - NodePrefs saved; - saved.radio_fem_rxgain = 0; - saved.radio_fem_txgain = 1; +TEST(DynamicConfigSerializer, GetSet_Basic) { + DynamicConfigSerializer data; - MockPrintStream output; - ASSERT_TRUE(saved.saveSerial(output)); + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); - std::string serialised(reinterpret_cast(output.getBytes()), output.getLength()); - EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0")); - EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1")); + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("11", tmp); - MockInputStream input(serialised.c_str()); - NodePrefs loaded; - loaded.radio_fem_rxgain = 1; - loaded.radio_fem_txgain = 0; - - ASSERT_TRUE(loaded.loadSerial(input)); - EXPECT_EQ(0, loaded.radio_fem_rxgain); - EXPECT_EQ(1, loaded.radio_fem_txgain); + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); } +TEST(DynamicConfigSerializer, Set_Replaces) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool s3 = data.setByKey("age", "333"); + EXPECT_TRUE(s3); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ("333", tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} + +TEST(DynamicConfigSerializer, GetUnknown_Fail) { + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + EXPECT_TRUE(s1); + + char tmp[32]; + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_FALSE(g2); +} + +TEST(DynamicConfigSerializer, SaveCustom_Basic) { + MockPrintStream s; + DynamicConfigSerializer data; + + bool s1 = data.setByKey("age", "11"); + bool s2 = data.setByKey("name", "Scott"); + EXPECT_TRUE(s1 && s2); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + char tmp[128]; + memcpy(tmp, s.getBytes(), l); + tmp[l] = 0; + + const char* expect = "{age:\"11\",name:\"Scott\"}"; + EXPECT_STREQ(expect, tmp); +} + +TEST(DynamicConfigSerializer, LoadCustom_Basic) { + MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}"); + DynamicConfigSerializer data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + char tmp[32]; + bool g1 = data.getByKey("age", tmp, 31); + EXPECT_TRUE(g1); + EXPECT_STREQ(TEST_INT_S, tmp); + + bool g2 = data.getByKey("name", tmp, 31); + EXPECT_TRUE(g2); + EXPECT_STREQ("Scott", tmp); +} // ── main ─────────────────────────────────────────────────────── From 6dad3d5ab4229fdbcbdf06bfedf4b3f228c985fd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Aug 2026 20:44:17 +1000 Subject: [PATCH 064/174] * companion: fix for setSpreadFactor(). "set cad ..." and "board" now implemented. --- examples/companion_radio/MyMesh.cpp | 7 ++++++- examples/companion_radio/NodePrefs.h | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 011df278..f5a4a5a9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -264,7 +264,7 @@ int MyMesh::getInterferenceThreshold() const { return 0; // disabled for now, until currentRSSI() problem is resolved } bool MyMesh::getCADEnabled() const { - return false; // hardware CAD before TX (disabled by default, until configurable) + return _prefs.cad_enabled; } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -2102,6 +2102,11 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "board") == 0) { + strcpy(reply, board.getManufacturerName()); + return true; + } + if (strcmp(command, "ver") == 0) { sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE); return true; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c79e2f0f..e45915b7 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -40,6 +40,7 @@ public: uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + uint8_t cad_enabled = 0; char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -52,7 +53,7 @@ private: def("bw", _parent->bw); def("sf", _parent->sf); def("cr", _parent->cr); - //def("cad", _parent->cad_enabled); + def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously @@ -75,7 +76,7 @@ private: float getBandwidth() const override { return _parent->bw; } void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); } uint8_t getSpreadFactor() const override { return _parent->sf; } - void setSpreadFactor(uint8_t sf) override { _parent->sf; markDirty(); } + void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); } uint8_t getCodingRate() const override { return _parent->cr; } void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } From 9ab13158cb62dc82222c0b7de567edabbe204cc3 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 25 Aug 2026 19:04:32 +1000 Subject: [PATCH 065/174] * fix for RPI Picow --- variants/rpi_picow/PicoWBoard.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/variants/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index 708e9655..e64590f2 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -2,6 +2,7 @@ #include #include +#include // built-ins #define PIN_VBAT_READ 26 @@ -16,6 +17,8 @@ public: void begin(); uint8_t getStartupReason() const override { return startup_reason; } + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op + void onBeforeTransmit() override { digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on } From 0908b034911adf827019ef58fd5c4b4a3b769320 Mon Sep 17 00:00:00 2001 From: Alex Beal Date: Tue, 25 Aug 2026 15:00:45 -0600 Subject: [PATCH 066/174] Fix: Do not misidentify IMU as an RTC on Heltec T1 The T1 has an ICM-42607-P IMU with the same address as a DS3231 RTC. This completely breaks the clock on the T1. Reading the time results in a garbage value, causing all sorts of strange issues. The time never advances, which breaks logging in to repeaters (due to their time-based replay protection). This simply disables the probe on the I2C line. The DS3231 doesn't have a WHO_AM_I register that could be used to prevent misidentifying it, so this simpler fix is used. --- variants/heltec_t1/variant.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/variants/heltec_t1/variant.h b/variants/heltec_t1/variant.h index b52aeba0..de40ea64 100644 --- a/variants/heltec_t1/variant.h +++ b/variants/heltec_t1/variant.h @@ -9,6 +9,12 @@ #define USE_LFXO #define VARIANT_MCK (64000000ul) +// This board has no external RTC chip -- the ICM-42607-P IMU sits at 0x68, +// the same I2C address DS3231 probing checks, so without this the IMU gets +// misidentified as a DS3231 and its unrelated registers get read/written +// as if they were time/status registers, producing a bogus, unfixable clock. +#define DISABLE_DS3231_PROBE + //////////////////////////////////////////////////////////////////////////////// // Number of pins From e3d0f9fcaaf69d0b57aa7489ecc052fae792c23a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 27 Aug 2026 19:05:18 +1200 Subject: [PATCH 067/174] fix cad cli command on companion --- examples/companion_radio/NodePrefs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index e45915b7..c8a9ab83 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -81,8 +81,8 @@ private: void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); } float getAirtimeFactor() const override { return _parent->airtime_factor; } void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } - bool isCadEnabled() const override { return false; } - void setCadEnabled(bool en) override { /* no-op */ } + bool isCadEnabled() const override { return _parent->cad_enabled; } + void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } uint8_t getIntThresh() const override { return 0; } void setIntThresh(uint8_t t) override { /* no-op */ } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } From 65650bb19268388a0ba72d6d34bab939117db978 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 28 Aug 2026 13:12:59 +1000 Subject: [PATCH 068/174] New TXT_TYPE_CLI_COMMAND (3) --- docs/payloads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/payloads.md b/docs/payloads.md index a2945e27..fb9cbaf9 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -171,8 +171,9 @@ txt_type | Value | Description | Message content | |--------|---------------------------|--------------------------------------------------------------------------| | `0x00` | plain text message | the plain text of the message | -| `0x01` | CLI command | the command text of the message | +| `0x01` | CLI data | CLI command OR reply text | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | +| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) | ## Anonymous request From e6897e8bb731275fe919ebeb94408303ff8ac8ed Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 29 Aug 2026 15:22:54 +1000 Subject: [PATCH 069/174] fix Meshnology W12 TX power --- variants/meshnology_w12/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 1168f10a..8255e46c 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -27,8 +27,8 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_VEXT_EN=45 -D PIN_VEXT_EN_ACTIVE=HIGH - -D LORA_TX_POWER=4 - -D MAX_LORA_TX_POWER=4 ; GC1109 datasheet says max input at TX port is +5dbm, confirmed full saturation seems to occur at around 3-4dbm + -D LORA_TX_POWER=5 ; default to 5, which gives ~22dbm + -D MAX_LORA_TX_POWER=13 ; tested with tinySA, 13 gives ~28dbm -D PIN_GPS_RX=38 -D PIN_GPS_TX=39 -D PIN_GPS_RESET=42 From 1d9a62ca4a77a4e43268d935ea0ae7d27ead9e59 Mon Sep 17 00:00:00 2001 From: Aleksei Mamlin Date: Mon, 31 Aug 2026 08:59:27 +0300 Subject: [PATCH 070/174] companion: get/set timezone offset for clock * Add tz_offset companion prefs and get/set commands for companion cli * Use tz_offset for clock on display Signed-off-by: Aleksei Mamlin --- examples/companion_radio/MyMesh.cpp | 16 ++++++++++++++++ examples/companion_radio/NodePrefs.h | 2 ++ examples/companion_radio/ui-new/UITask.cpp | 7 +------ variants/thinknode_m8/platformio.ini | 1 - 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b122b605..ee8114ca 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2166,6 +2166,22 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "get tz.offset") == 0) { + sprintf(reply, "> %d", _prefs.tz_offset); + return true; + } + if (memcmp(command, "set tz.offset ", 14) == 0) { + int8_t tz = atof(&command[14]); + if (tz < -12 || tz > 14) { + strcpy(reply, "Error, must be from -12 to +14"); + } else { + _prefs.tz_offset = tz; + savePrefs(); + strcpy(reply, "OK"); + } + return true; + } + return false; // not handled } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c8a9ab83..f6f9b887 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -43,6 +43,7 @@ public: uint8_t cad_enabled = 0; char default_scope_name[31]; uint8_t default_scope_key[16]; + int8_t tz_offset = 0; private: class RadioPrefs : public CommonRadioPrefs { @@ -150,6 +151,7 @@ private: def("tel_base", _parent->telemetry_mode_base); def("tel_loc", _parent->telemetry_mode_loc); def("tel_env", _parent->telemetry_mode_env); + def("tz_offset", _parent->tz_offset); } public: CompanionPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a7227bd8..2c79ab91 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -7,10 +7,6 @@ #include #endif -#ifndef UI_TZ_OFFSET - #define UI_TZ_OFFSET 0 -#endif - #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif @@ -256,8 +252,7 @@ public: #ifdef UI_SHOW_CLOCK display.setTextSize(3); uint32_t now = _rtc->getCurrentTime(); - int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ... - now += (int32_t)tz * 3600; + now += (int32_t)_node_prefs->tz_offset * 3600; DateTime dt (now); sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute()); display.drawTextCentered(display.width() / 2, 60, tmp); diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index b8c70973..db19e446 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -22,7 +22,6 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_ROTARY_INPUT=1 -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 - -D UI_TZ_OFFSET=-4 # GMT-4 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 From f66ff1d13fd30dceb6bd4bea29ef5b54a06841fd Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:01 -0500 Subject: [PATCH 071/174] docs: correct stale CLI default values Five "Default:" values in cli_commands.md no longer matched the firmware: - radio / freq: the default preset moved to EU/UK (Narrow) in b777a7c6, changing LORA_FREQ/BW/SF from 869.525/250/11 to 869.618/62.5/8 (platformio.ini:29-31). No variant overrides these, and LORA_CR is 5 on every path, so the full preset is 869.618,62.5,8,5. - flood.advert.interval: raised to 47 hours in 40180b8f for both repeater and room server; sensor leaves it disabled. - advert.interval: prefs store minutes/2 and the getter doubles on read, so a factory-fresh node reports 2, not 0. But savePrefs() zeroes any interval below the 60 minute minimum (CommonCLI.cpp:162-165), and it is called from every `set` handler -- so the value becomes 0 as soon as the node is configured. Documented both states, since neither alone is the whole story. - direct.txdelay: repeater defaults to 0.3, room server and sensor to 0.2. Sources: platformio.ini:29-31, simple_repeater/MyMesh.cpp:893,903-904, simple_room_server/MyMesh.cpp:650,661-662, simple_sensor/SensorMesh.cpp:716, 726-727, CommonCLI.cpp:162-165,680-681. --- docs/cli_commands.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929..f4a43fad 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -211,7 +211,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Set by build flag:** `LORA_FREQ`, `LORA_BW`, `LORA_SF`, `LORA_CR` -**Default:** `869.525,250,11,5` +**Default:** `869.618,62.5,8,5` **Note:** Requires reboot to apply @@ -256,7 +256,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `frequency`: Frequency in MHz -**Default:** `869.525` +**Default:** `869.618` **Note:** Requires reboot to apply **Serial Only:** `set freq ` @@ -537,7 +537,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.2` +**Default:** `0.3` (Repeater) - `0.2` (Room Server, Sensor) **Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. @@ -654,7 +654,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `hours`: Interval in hours (3-168) -**Default:** `12` (Repeater) - `0` (Sensor) +**Default:** `47` (Repeater, Room Server) - `0`, disabled (Sensor) --- @@ -666,7 +666,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `minutes`: Interval in minutes rounded down to the nearest multiple of 2 (61 becomes 60) (60-240) -**Default:** `0` +**Default:** `2` on a factory-fresh node, then `0` (disabled) once configured. + +**Note:** A new install ships with a 2 minute zero-hop advert interval. Saving +any setting resets an interval below the 60 minute minimum to `0`, on the +assumption that the node has now been deliberately configured. To keep zero-hop +adverts running, set an explicit value in the 60-240 range. --- From f5b6ff4c14fd2bd29688bc3b64fc39e0517839ea Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:42 -0500 Subject: [PATCH 072/174] docs: fix channel index range, secret semantics and payload limit Three factual errors in companion_protocol.md: - The channel-datagram payload cap was given as 163 (MAX_FRAME_SIZE - 9 when MAX_FRAME_SIZE was still 172). It went 172 -> 176 in 62f1b11d, but simply updating the arithmetic to 167 would be worse than the stale value: 167 is only the host-frame bound (MyMesh.cpp:1265). The radio-side bound is MAX_GROUP_DATA_LENGTH = 184 - 16 - 3 = 165 (MeshCore.h:21, enforced at BaseChatMesh.cpp:544). A 166- or 167-byte payload passes the frame check, fails the radio check, and comes back as ERR_CODE_TABLE_FULL -- which this same doc describes as "retry later", so a conforming client would retry a permanently failing send forever. Documents 165 as the limit to enforce and calls out the 166-167 band explicitly. - Channel index was documented as 0-7 throughout. The bound is MAX_GROUP_CHANNELS (BaseChatMesh.cpp:927,936), which is 40 on most current variants, 8 on some and 1 on others. Clients should read max_channels from byte 3 of PACKET_DEVICE_INFO instead. Index 0 is pre-populated with the built-in Public channel but is not reserved. - The secret field was documented as "all zeros" for public channels. The firmware always hashes a real 16-byte key; the public channel ships with izOH6cXN6mrJ5e26oRXNcg== (companion_radio/MyMesh.cpp:111,1040). An all-zero secret is not a private channel and not an inert one: SHA256 over 16 zero bytes is a fixed global constant, giving a well-known channel with an all-zero AES key. searchChannelsByHash skips unnamed slots for exactly this reason (BaseChatMesh.cpp:392-401), but a named slot with a zero secret is not skipped and will absorb null-key group traffic from any node. Now documented as something not to do. --- docs/companion_protocol.md | 54 +++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 8c6b84b9..12d81bc4 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -206,7 +206,7 @@ Byte 1: 0x03 **Command Format**: ``` Byte 0: 0x1F -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) ``` **Example** (get channel 1): @@ -225,7 +225,7 @@ Byte 1: Channel Index (0-7) **Command Format**: ``` Byte 0: 0x20 -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded) Bytes 34-49: Secret (16 bytes) ``` @@ -233,8 +233,12 @@ Bytes 34-49: Secret (16 bytes) **Total Length**: 50 bytes **Channel Index**: -- Index 0: Reserved for public channels (no secret) -- Indices 1-7: Available for private channels +- Valid range is `0` to `max_channels - 1`. Read `max_channels` from byte 3 of + the `PACKET_DEVICE_INFO` response; do not assume 8. Builds ship with + `MAX_GROUP_CHANNELS` set to 1, 8 or 40 depending on the board. +- Index 0: pre-populated by the firmware with the built-in "Public" channel. + It can be overwritten like any other slot. +- An out-of-range index is rejected with `PACKET_ERROR` / `ERR_CODE_NOT_FOUND`. **Channel Name**: - UTF-8 encoded @@ -242,8 +246,18 @@ Bytes 34-49: Secret (16 bytes) - Padded with null bytes (0x00) if shorter **Secret Field** (16 bytes): -- For **private channels**: 16-byte secret -- For **public channels**: All zeros (0x00) +- Always a real 16-byte key; the firmware derives the channel hash from + `SHA256(secret)` (`BaseChatMesh.cpp:936-942`). There is no "no secret" form. +- Do **not** write an all-zero secret. `SHA256` over 16 zero bytes is a fixed, + globally identical value, so a zero-secret channel is a well-known channel + with an all-zero AES key that any node can read — not a private one. The + firmware skips *unnamed* slots when matching inbound group traffic for + exactly this reason (`BaseChatMesh.cpp:392-401`), but a slot that has been + given a name and a zero secret is not skipped, and will absorb null-key + group traffic from any node on the mesh. +- The built-in public channel uses the well-known key + `izOH6cXN6mrJ5e26oRXNcg==` (base64), i.e. + `8b3387e9c5cdea6ac9e5edbaa115cd72` in hex. **Example** (create channel "YourChannelName" at index 1 with secret): ``` @@ -265,7 +279,7 @@ Bytes 34-49: Secret (16 bytes) ``` Byte 0: 0x03 Byte 1: 0x00 -Byte 2: Channel Index (0-7) +Byte 2: Channel Index (0 .. max_channels-1) Bytes 3-6: Timestamp (32-bit little-endian Unix timestamp, seconds) Bytes 7+: Message Text (UTF-8, variable length) ``` @@ -288,7 +302,7 @@ Bytes 7+: Message Text (UTF-8, variable length) **Command Format**: ``` Byte 0: 0x3E -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length (0xFF = flood, otherwise actual path length) Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF) Next 2 bytes (little-endian): Data Type (`data_type`, uint16) @@ -306,13 +320,23 @@ Remaining bytes: Binary payload (variable length) - Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below. **Limits**: -- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes. -- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`). +- Maximum payload length is **165** bytes — `MAX_GROUP_DATA_LENGTH = + MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3` (`src/MeshCore.h:21`). This is the + radio-side limit and the one clients should enforce. +- Two different bounds are checked, and they do not agree. The host-frame check + uses the larger `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 167` + (`companion_radio/MyMesh.cpp:1265`); the radio-side check uses 165 + (`BaseChatMesh.cpp:544`). +- Payloads **above 167** are rejected with `ERR_CODE_ILLEGAL_ARG` (6). +- Payloads of **166 or 167** pass the frame check, then fail the radio-side + check and return `ERR_CODE_TABLE_FULL` (3). Despite that code's usual + meaning, such a send can never succeed — do not retry it. Keep payloads at + 165 bytes or fewer and this case cannot arise. **Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of: - `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx` -- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` -- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later +- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH` (167) +- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full (retry later), **or** a payload of 166-167 bytes that exceeds the radio-side limit (permanent; see Limits above) **Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram). @@ -341,7 +365,7 @@ Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarde Byte 0: 0x1B (packet type) Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB) Bytes 2-3: Reserved (clients MUST ignore) -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct) Bytes 6-7: Data Type (uint16 little-endian) Byte 8: Data Length @@ -550,7 +574,7 @@ def parse_contact_message(data): **Standard Format** (`PACKET_CHANNEL_MSG_RECV`, 0x08): ``` Byte 0: 0x08 (packet type) -Byte 1: Channel Index (0-7) +Byte 1: Channel Index (0 .. max_channels-1) Byte 2: Path Length Byte 3: Text Type Bytes 4-7: Timestamp (32-bit little-endian) @@ -562,7 +586,7 @@ Bytes 8+: Message Text (UTF-8) Byte 0: 0x11 (packet type) Byte 1: SNR (signed byte, multiplied by 4) Bytes 2-3: Reserved -Byte 4: Channel Index (0-7) +Byte 4: Channel Index (0 .. max_channels-1) Byte 5: Path Length Byte 6: Text Type Bytes 7-10: Timestamp (32-bit little-endian) From 208333d51159e0b06ba32f4c6ed22b8bd9c33881 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:27:56 -0500 Subject: [PATCH 073/174] docs: correct pwrmgt CLI behaviour on unsupported boards The doc claimed every pwrmgt command except `get pwrmgt.support` returns "ERROR: Power management not supported" when NRF52_POWER_MANAGEMENT is not defined. `get pwrmgt.bootreason` is not inside the #ifdef (CommonCLI.cpp:787) and answers on all boards; only pwrmgt.source and pwrmgt.bootmv are gated. --- docs/nrf52_power_management.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 6d97f2c3..417ee6dd 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -186,11 +186,19 @@ Power management status can be queried via the CLI: | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | -On boards without power management enabled, all commands except `get pwrmgt.support` return: +On boards without power management enabled, `get pwrmgt.source` and +`get pwrmgt.bootmv` return: ``` ERROR: Power management not supported ``` +`get pwrmgt.support` returns `unsupported`. `get pwrmgt.bootreason` is not +compiled out at all and answers on every board: `getResetReason()` and +`getShutdownReason()` are virtuals on the base board class, so a board that +does not override them reports `Not available` rather than an error. ESP32 +boards override the reset half with `esp_reset_reason()`, so they return a real +reset reason and `Not available` for the shutdown reason. + ## Debug Output When `MESH_DEBUG=1` is enabled, the power management module outputs: From fe5d4cc50dac989157fa2d63410fafc5786ee915 Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:28:17 -0500 Subject: [PATCH 074/174] docs: remove non-existent set dutycycle from Terminal Chat CLI f6338430 added get/set dutycycle to CommonCLI and updated both CLI docs, but Terminal Chat does not use CommonCLI -- simple_secure_chat/main.cpp:479-507 has its own `set` handler supporting only af, name, lat, lon, tx and freq. Typing `set dutycycle` there returns "ERROR: unknown config". Restores `set af` as the documented command, points readers at cli_commands.md for the roles that do have dutycycle, and documents `help` (simple_secure_chat/main.cpp:510), which the client implements but the doc never listed. --- docs/terminal_chat_cli.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index 31616144..d5bac7ed 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -27,15 +27,17 @@ set lon {longitude} ``` Sets your advertisement map longitude. (decimal degrees) -``` -set dutycycle {percent} -``` -Sets the transmit duty cycle limit (1-100%). Example: `set dutycycle 10` for 10%. - ``` set af {air-time-factor} ``` -Sets the transmit air-time-factor. Deprecated — use `set dutycycle` instead. +Sets the transmit air-time-factor. + +> **Note:** `set dutycycle` is *not* available in Terminal Chat. Every other +> firmware — Repeater, Room Server, Sensor and Companion Radio — routes its +> `set`/`get` commands through the shared radio prefs handler and does support +> it (see [CLI Commands](./cli_commands.md)). Terminal Chat is the sole +> exception: it has its own inline `set` handler covering only the six options +> listed above. ``` @@ -99,3 +101,8 @@ Resets the path to current recipient, for new path discovery. public {text} ``` Sends the text message to the built-in 'public' group channel + +``` +help +``` +Lists the available commands. From 90a4f51f915cb78c5882299b8c68cadaf8fe0b6c Mon Sep 17 00:00:00 2001 From: Robert Ekl Date: Wed, 2 Sep 2026 11:28:51 -0500 Subject: [PATCH 075/174] docs: update stale repo URLs and build recipe The project moved to meshcore-dev/meshcore, but two links still pointed at ripplebiz/MeshCore: the GitHub Issues link in README and the git clone in the FAQ build instructions. Also fixes the FAQ build recipe: - `pio run -e RAK_4631_Repeater` does not exist; the env is spelled RAK_4631_repeater (variants/rak4631/platformio.ini:40). - LORA_FREQ was given as 867.5; the default in [arduino_base] is 869.618 (platformio.ini:29). Reworded to point at the flag rather than a value that drifts with the default preset. - Renamed the venv so it no longer collides with the cloned directory name. And one more env-name casing slip in the Raspberry Pi flashing instructions: `Heltec_V3_companion_radio_ble` should be `Heltec_v3_...`. Release artifacts are named directly from the env name (build.sh:147), and the two neighbouring examples in the same block already use the lowercase spelling. --- README.md | 2 +- docs/faq.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af56aaf3..6cc45d7f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,6 @@ There are a number of fairly major features in the pipeline, with no particular ## 📞 Get Support -- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. +- Report bugs and request features on the [GitHub Issues](https://github.com/meshcore-dev/meshcore/issues) page. - Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz). - Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community. diff --git a/docs/faq.md b/docs/faq.md index c7d7a118..ade59bf0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -537,18 +537,18 @@ Mac: python3 should be already installed. Then it should be the same for all platforms: ``` -python3 -m venv meshcore -cd meshcore && source bin/activate +python3 -m venv meshcore-venv +cd meshcore-venv && source bin/activate pip install -U platformio -git clone https://github.com/ripplebiz/MeshCore.git -cd MeshCore +git clone https://github.com/meshcore-dev/meshcore.git +cd meshcore ``` -open platformio.ini and in `[arduino_base]` edit the `LORA_FREQ=867.5` -save, then run: +open platformio.ini and in `[arduino_base]` edit `LORA_FREQ` (it defaults to +`869.618`) to the frequency for your region, save, then run: ``` -pio run -e RAK_4631_Repeater +pio run -e RAK_4631_repeater ``` -then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater` +then you'll find `firmware.zip` in `.pio/build/RAK_4631_repeater` ### 5.10. Q: Are there other MeshCore related open source projects? @@ -592,7 +592,7 @@ For ESP-based devices (e.g. Heltec V3) you need: 1. Download the firmware file from . - Go to the website in a browser and find the section that has the firmware you need. - Click the Download button, right-click on the file you need, for example: - - `Heltec_V3_companion_radio_ble-v1.7.1-165fb33.bin` + - `Heltec_v3_companion_radio_ble-v1.7.1-165fb33.bin` - Non-merged bin keeps the existing Bluetooth pairing database. - `Heltec_v3_companion_radio_usb-v1.7.1-165fb33-merged.bin` - Merged bin overwrites everything including the bootloader and existing Bluetooth pairing database, but keeps configurations. From b4e37044edf55ce96a7a4e14261ef3ec711faa68 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 3 Sep 2026 14:09:15 +1000 Subject: [PATCH 076/174] * default and reply scopes now applied --- examples/simple_sensor/SensorMesh.cpp | 76 ++++++++++++++++++++++----- examples/simple_sensor/SensorMesh.h | 6 +++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 23d0cdc3..287a3c1b 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,4 +1,5 @@ #include "SensorMesh.h" +#include /* ------------------------------ Config -------------------------------- */ @@ -262,7 +263,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { sendDirect(pkt, c->out_path, c->out_path_len); } else { unsigned long delay_millis = 0; - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } } t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); @@ -400,10 +401,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r if (sp == NULL) { strcpy(reply, "Err - bad params"); } else { + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + uint8_t pubkey[PUB_KEY_SIZE]; + *sp++ = 0; // replace space with null terminator - uint8_t pubkey[PUB_KEY_SIZE]; - int hex_len = min(sp - hex, PUB_KEY_SIZE*2); if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { uint8_t perms = atoi(sp); if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { @@ -451,6 +453,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r } } +mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) { + if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { + if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + recv_pkt_region = NULL; + } else { + recv_pkt_region = ®ion_map.getWildcard(); + } + } else { + recv_pkt_region = NULL; + } + return Mesh::onRecvPacket(pkt); +} + void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; @@ -472,10 +489,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -503,7 +520,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); + if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -541,14 +558,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); if (reply) { if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); + sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -571,7 +588,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); + if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); } @@ -601,7 +618,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); if (reply) { if (from->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -708,6 +725,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise last_read_time = 0; num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; + recv_pkt_region = NULL; // defaults _prefs.airtime_factor = 1.0; @@ -820,11 +838,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params } +void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) { + if (scope.isNull()) { + sendFlood(pkt, delay_millis, path_hash_size); + } else { + uint16_t codes[2]; + codes[0] = scope.calcTransportCode(pkt); + codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? + sendFlood(pkt, codes, delay_millis, path_hash_size); + } +} + +void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + TransportKey req_scope; + bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard(); + bool req_scope_known = recv_pkt_region != NULL && !is_wildcard + && region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0; + + switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) { + case mesh::REPLY_SCOPE_REQUEST: + sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request + break; + case mesh::REPLY_SCOPE_DEFAULT: + // requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region. + // un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0 + sendFloodScoped(default_scope, packet, delay_millis, path_hash_size); + break; + case mesh::REPLY_SCOPE_NONE: + sendFlood(packet, delay_millis, path_hash_size); // send un-scoped + break; + } +} + void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -902,7 +952,7 @@ void SensorMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); unsigned long delay_millis = 0; - if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); + if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); // schedule next flood advert updateAdvertTimer(); // also schedule local advert (so they don't overlap) @@ -973,7 +1023,7 @@ void SensorMesh::loop() { } } - // is there are pending dirty contacts write needed? + // pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); dirty_contacts_expiry = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5c..845402ba 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -81,6 +81,9 @@ public: float getTelemValue(uint8_t channel, uint8_t type); + void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); + void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + protected: // current telemetry data queries float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } @@ -122,6 +125,7 @@ protected: int getInterferenceThreshold() const override; bool getCADEnabled() const override; int getAGCResetInterval() const override; + mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; @@ -129,6 +133,7 @@ protected: bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; void onControlDataRecv(mesh::Packet* packet) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; + virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); private: @@ -142,6 +147,7 @@ private: CayenneLPP telemetry; TransportKeyStore key_store; RegionMap region_map; + RegionEntry* recv_pkt_region; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; From 7d501681bc3f1e93c9b404138fc143195b70cdc9 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 3 Sep 2026 17:57:09 +0800 Subject: [PATCH 077/174] fix-heltec_v4r8-lna --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 58 +++++++++++++++++++++++ variants/heltec_v4_r8/HeltecV4R8Board.h | 7 +++ variants/heltec_v4_r8/LoRaFEMControl.h | 1 + 3 files changed, 66 insertions(+) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1fb123b2..ea2cfbed 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -84,3 +84,61 @@ const char* HeltecV4R8Board::getManufacturerName() const { return "Heltec V4 R8 OLED"; #endif } + +bool HeltecV4R8Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.isLnaCanControl()) { + return false; + } + + loRaFEMControl.setLNAEnable(enable); + loRaFEMControl.setRxModeEnable(); + return true; +} + +bool HeltecV4R8Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} + +void HeltecV4R8Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char radio_fem_rxgain[8] = { 0 }; + _prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values + + setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0); +} + +bool HeltecV4R8Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off"); + } + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + if (!loRaFEMControl.isLnaCanControl()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemLnaEnabled(true)) { + _prefs->setByKey("fem_rxgain", "1"); + strcpy(reply, "OK - LoRa FEM RX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemLnaEnabled(false)) { + _prefs->setByKey("fem_rxgain", "0"); + strcpy(reply, "OK - LoRa FEM RX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM RX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index 20811abb..7ab29f96 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -11,6 +11,10 @@ #endif class HeltecV4R8Board : public ESP32Board { + KeyValueStore* _prefs = NULL; + + bool setLoRaFemLnaEnabled(bool enable); + bool isLoRaFemLnaEnabled() const; protected: float adc_mult = ADC_MULTIPLIER; @@ -21,6 +25,9 @@ public: HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h index 961cfd07..4b74c2e2 100644 --- a/variants/heltec_v4_r8/LoRaFEMControl.h +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -17,6 +17,7 @@ public: void setLNAEnable(bool enabled); bool isLnaCanControl(void) { return true; } void setLnaCanControl(bool can_control) { } + bool isLNAEnabled(void) const { return lna_enabled; } LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } private: From 9bc6060831a0ad6064911836518e143d8a7c5ab9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 15:13:17 +1000 Subject: [PATCH 078/174] * new: REQ_TYPE_SUBSCRIBE and REQ_TYPE_UNSUBSCRIBE --- examples/simple_sensor/SensorMesh.cpp | 124 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 9 +- src/helpers/ClientACL.h | 5 ++ 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 287a3c1b..0a78f5da 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,6 +55,8 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_SUBSCRIBE 0x10 +#define REQ_TYPE_UNSUBSCRIBE 0x11 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -74,6 +76,8 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { #endif } +/* --------------------- Cayenne LPP helpers ----------------------------*/ + static uint8_t getDataSize(uint8_t type) { switch (type) { case LPP_GPS: @@ -171,7 +175,53 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult return size; } -uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + if (ch == channel && t == type) { + return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + } + i += sz; // skip + } + return 0.0f; // not found +} + +/* ------------------ end Cayenne LPP helpers ----------------------*/ + +bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { + if (telemetry.getSize() != prev_telem_size) return true; + + auto buf = telemetry.getBuffer(); + uint8_t size = telemetry.getSize(); + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + if (abs(v - pv) > min_delta) return true; // Yes, has changed + + i += sz; // skip + } + return false; // no changes +} + +uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all @@ -235,6 +285,29 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint return ofs; } } + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint8_t reserved = payload[0]; + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + from->extra.sensor.min_deltas_len = payload[1]; + // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) + memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + + getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness + strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[6]); + } + if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + from->extra.sensor.scope_region_id = 0; + reply_data[4] = 0; // success + getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness + return 8; + } return 0; // unknown command } @@ -548,7 +621,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(×tamp, data, 4); if (timestamp > from->last_timestamp) { // prevent replay attacks - uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5); + uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5); if (reply_len == 0) return; // invalid command from->last_timestamp = timestamp; @@ -726,6 +799,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; + prev_telem_size = 0; // defaults _prefs.airtime_factor = 1.0; @@ -916,23 +990,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - auto buf = telemetry.getBuffer(); - uint8_t size = telemetry.getSize(); - uint8_t i = 0; - - while (i + 2 < size) { - // Get channel # - uint8_t ch = buf[i++]; - // Get data type - uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); - - if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - } - i += sz; // skip - } - return 0.0f; // not found + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { @@ -982,6 +1040,34 @@ void SensorMesh::loop() { // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // compare with previous telemetry, check if any deltas are greater than subscriber minimums + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); + if (r == NULL) continue; // unknown region scope + if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + TransportKey scope; + if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { + uint8_t tlen = telemetry.getSize(); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client + memcpy(reply_data, ×tamp, 4); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + if (reply) { + if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + sendDirect(reply, c->out_path, c->out_path_len, 0); + } else { + sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1); + } + } + } + } + } + memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle + prev_telem_size = telemetry.getSize(); + onSensorDataRead(); last_read_time = curr; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 845402ba..7ea87f20 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -96,12 +96,12 @@ protected: bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); // alerts - enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT }; struct Trigger { uint32_t timestamp; - AlertPriority pri; uint32_t expected_acks[4]; + AlertPriority pri; int8_t curr_contact_idx; uint8_t attempt; unsigned long send_expiry; @@ -145,6 +145,8 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; + uint8_t prev_telem_size; + uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map; RegionEntry* recv_pkt_region; @@ -159,8 +161,9 @@ private: uint8_t pending_sf; uint8_t pending_cr; + bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); - uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); void sendAlert(const ClientInfo* c, Trigger* t); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index e0654464..9fde35ec 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -28,6 +28,11 @@ struct ClientInfo { unsigned long ack_timeout; uint8_t push_failures; } room; + struct { + uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber + uint8_t min_deltas_len; + uint8_t min_deltas[14]; // LPP encoded + } sensor; } extra; bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } From 3a5a6b90ab3d9e65950dd4e8e03026ce73da4103 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 4 Sep 2026 22:02:49 +1000 Subject: [PATCH 079/174] * sensor: CLI "sub" and "unsub" commands --- examples/simple_sensor/SensorMesh.cpp | 68 ++++++++++++++++++++++++++- examples/simple_sensor/SensorMesh.h | 2 +- examples/simple_sensor/main.cpp | 2 +- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 0a78f5da..008cbcfc 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -193,6 +193,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } +static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { + const char* parts[3]; + int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); + uint8_t len = 0; + for (int i = 0; i < n && len + 6 <= max_len; i++) { + const char* cp = strchr(parts[i], ':'); + if (cp) { + uint8_t t; + float factor = 1.0f; + cp++; // skip the ':' + char* ep = strchr(cp, 0) - 1; // find LAST char + if (*ep == 'V') { // Volts + t = LPP_VOLTAGE; + } else if (*ep == 'W') { // Watts + t = LPP_POWER; + } else if (*ep == 'C') { // Celcius + t = LPP_TEMPERATURE; + } else if (*ep == 'P') { // Pascals + t = LPP_BAROMETRIC_PRESSURE; + } else if (*ep == 'A') { // Amps + t = LPP_CURRENT; + } else if (*ep == 'm') { + t = LPP_DISTANCE; factor = 0.001f; + } else { + t = 0; + } + + if (t) { + dest[len++] = atoi(parts[i]); // channel number + dest[len++] = t; // LPP type + len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); + } + } + } + return len; +} + /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -453,7 +490,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* return 13; // reply length } -void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { +void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -502,6 +539,33 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r Serial.printf("\n"); } reply[0] = 0; + } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe + uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; + if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + RegionEntry* r; + if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope + r = recv_pkt_region; + } else { // use default scope + r = region_map.getDefaultRegion(); + } + if (command[3] == ' ') { + // compile params as LPP data, eg. "sub 1:0.2V" + from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } else { + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + } + from->extra.sensor.scope_region_id = r ? r->id : 0; + if (from->extra.sensor.scope_region_id) { + strcpy(reply, "OK - subscribed"); + } else { + strcpy(reply, "Err - region scope needed"); + } + } else { + strcpy(reply, "Err - no permission"); + } + } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe + from->extra.sensor.scope_region_id = 0; + strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -676,7 +740,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i uint8_t temp[166]; char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; - handleCommand(sender_timestamp, command, reply); + handleCommand(from, sender_timestamp, command, reply); int text_len = strlen(reply); if (text_len > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 7ea87f20..6d5857d0 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -51,7 +51,7 @@ public: SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); void loop(); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply); // CommonCLI callbacks const char* getFirmwareVer() override { return FIRMWARE_VERSION; } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a..9e3b230b 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -137,7 +137,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } From 95214e8c40264eb374ed2a94d6905619ae557650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Thu, 16 Jul 2026 19:59:12 +0000 Subject: [PATCH 080/174] Ensure command buffers stay NUL-terminated to prevent overflow The serial command buffers must stay NUL-terminated within their bounds: if they ever aren't, strlen() can return >= sizeof(command) and the read loop would index past the buffer. Additionally, a full buffer now becomes a completed line (end-of-line marker placed inside the buffer, NUL terminator kept) instead of overwriting the terminator and silently corrupting the buffer for the next pass. Applies to the serial CLI readers of the repeater, room server, sensor and secure chat examples, and to the CLI rescue reader of the companion example. --- examples/companion_radio/MyMesh.cpp | 12 ++++++++++-- examples/simple_repeater/main.cpp | 12 ++++++++++-- examples/simple_room_server/main.cpp | 12 ++++++++++-- examples/simple_secure_chat/main.cpp | 12 ++++++++++-- examples/simple_sensor/main.cpp | 12 ++++++++++-- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ee8114ca..b4e51514 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2187,6 +2187,13 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* void MyMesh::checkCLIRescueCmd() { int len = strlen(cli_command); + // `cli_command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(cli_command) and the loop below would + // then index past the buffer, so clamp defensively. + if (len >= (int)sizeof(cli_command)) { + cli_command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(cli_command)-1) { char c = Serial.read(); if (c != '\n') { @@ -2195,8 +2202,9 @@ void MyMesh::checkCLIRescueCmd() { } Serial.print(c); // echo } - if (len == sizeof(cli_command)-1) { // command buffer full - cli_command[sizeof(cli_command)-1] = '\r'; + if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line + cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer + cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && cli_command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68..1f71da74 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -125,6 +125,13 @@ void setup() { void loop() { // Handle Serial CLI int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -134,8 +141,9 @@ void loop() { } if (c == '\r') break; } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff3..227ee2cb 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -105,6 +105,13 @@ void setup() { void loop() { int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -113,8 +120,9 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 159249df..2e6ba712 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -530,6 +530,13 @@ public: BaseChatMesh::loop(); int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -538,8 +545,9 @@ public: } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a..749ff6ef 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -122,6 +122,13 @@ void setup() { void loop() { int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -130,8 +137,9 @@ void loop() { } Serial.print(c); } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line From 1d5283de767881e5f9c5ad2b4f564d422779df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Wed, 15 Jul 2026 08:18:59 +0000 Subject: [PATCH 081/174] Improve null checks and index validation in StaticPoolPacketManager --- src/helpers/StaticPoolPacketManager.cpp | 9 +++++++-- src/helpers/StaticPoolPacketManager.h | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index b8926df0..8a8eefe8 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -44,7 +44,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) { } mesh::Packet* PacketQueue::removeByIdx(int i) { - if (i >= _num) return NULL; // invalid index + if (i < 0 || i >= _num) return NULL; // invalid index mesh::Packet* item = _table[i]; _num--; @@ -80,10 +80,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() { } void StaticPoolPacketManager::free(mesh::Packet* packet) { - unused.add(packet, 0, 0); + if (packet == NULL) return; + if (!unused.add(packet, 0, 0)) { + MESH_DEBUG_PRINTLN("free: unused queue full, possible double-free detected"); + } } void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { + if (packet == NULL) return; if (!send_queue.add(packet, priority, scheduled_for)) { MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); free(packet); @@ -115,6 +119,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { } void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { + if (packet == NULL) return; if (!rx_queue.add(packet, 0, scheduled_for)) { MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet"); free(packet); diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 59715b4e..f63700e5 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -14,7 +14,10 @@ public: bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); int count() const { return _num; } int countBefore(uint32_t now) const; - mesh::Packet* itemAt(int i) const { return _table[i]; } + mesh::Packet* itemAt(int i) const { + if (i < 0 || i >= _num) return NULL; + return _table[i]; + } mesh::Packet* removeByIdx(int i); }; From 8a418faf93c5a0b94c1b60c8a7da8b811a43b002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Fri, 4 Sep 2026 21:04:20 +0100 Subject: [PATCH 082/174] Add isGPSDetected() capability probe to SensorManager Lets applications know whether a GPS was actually detected (EnvironmentSensorManager reports its gps_detected state), e.g. to hide GPS-dependent UI when no receiver is attached. --- src/helpers/SensorManager.h | 1 + src/helpers/sensors/EnvironmentSensorManager.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c2..85b9a1ee 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -23,6 +23,7 @@ public: virtual const char* getSettingValue(int i) const { return NULL; } virtual bool setSettingValue(const char* name, const char* value) { return false; } virtual LocationProvider* getLocationProvider() { return NULL; } + virtual bool isGPSDetected() const { return false; } // Helper functions to manage setting by keys (useful in many places ...) const char* getSettingByKey(const char* key) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c89..17706cbc 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -38,6 +38,7 @@ public: #if ENV_INCLUDE_GPS EnvironmentSensorManager(LocationProvider &location): _location(&location){}; LocationProvider* getLocationProvider() { return _location; } + bool isGPSDetected() const override { return gps_detected; } #else EnvironmentSensorManager(){}; #endif From 8fd689311772783b3fac106f002b51dd7b884e24 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 4 Sep 2026 16:03:41 -0700 Subject: [PATCH 083/174] feat: add T-Beam 1W thermal fan control --- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 341 +++++++++++++++++++++- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 39 ++- variants/lilygo_tbeam_1w/variant.h | 19 +- 3 files changed, 387 insertions(+), 12 deletions(-) diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index 1719d733..e82e4623 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -1,5 +1,12 @@ #include "TBeam1WBoard.h" +#include +#include +#include +#include + +static const int FAN_PWM_MAX = (1 << FAN_PWM_RES_BITS) - 1; + void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -15,31 +22,57 @@ void TBeam1WBoard::begin() { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); - // Initialize fan control (on by default - 1W PA can overheat) + // NTC ADC (PA-adjacent thermistor) + pinMode(NTC_PIN, INPUT); + analogSetPinAttenuation(NTC_PIN, ADC_11db); + analogReadResolution(12); + + // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); digitalWrite(FAN_CTRL_PIN, HIGH); + _temp_c = readNtcTempC(); + applyDuty(100); + startFanTask(); +} + +void TBeam1WBoard::startFanTask() { + if (_fan_task) return; + xTaskCreate(fanTaskThunk, "tbeam1w_fan", 4096, this, 1, &_fan_task); +} + +void TBeam1WBoard::fanTaskThunk(void* arg) { + auto* self = static_cast(arg); + for (;;) { + if (!self->_stopped) { + self->updateFan(); + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } } void TBeam1WBoard::onBeforeTransmit() { - // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() digitalWrite(LED_PIN, HIGH); // TX LED on + _tx_active = true; + if (_mode == FAN_AUTO && _manual_duty < 0 && !_stopped) { + applyDuty(FAN_TX_FLOOR_PCT); + } } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + _tx_active = false; } uint16_t TBeam1WBoard::getBattMilliVolts() { // T-Beam 1W uses 7.4V battery with voltage divider - // ADC reads through divider - adjust multiplier based on actual divider ratio analogReadResolution(12); uint32_t raw = 0; for (int i = 0; i < 8; i++) { raw += analogRead(BATTERY_PIN); } raw = raw / 8; - // Assuming voltage divider ratio from ADC_MULTIPLIER - // 3.3V reference, 12-bit ADC (4095 max) return static_cast((raw * 3300 * ADC_MULTIPLIER) / 4095); } @@ -48,6 +81,9 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + _stopped = true; + applyDuty(0); + // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -55,17 +91,304 @@ void TBeam1WBoard::powerOff() { digitalWrite(SX126X_POWER_EN, LOW); radio_powered = false; - // Turn off LED and fan digitalWrite(LED_PIN, LOW); - digitalWrite(FAN_CTRL_PIN, LOW); ESP32Board::powerOff(); } void TBeam1WBoard::setFanEnabled(bool enabled) { - digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); + applyDuty(enabled ? 100 : 0); } bool TBeam1WBoard::isFanEnabled() const { - return digitalRead(FAN_CTRL_PIN) == HIGH; + return _duty_pct > 0; +} + +float TBeam1WBoard::readNtcTempC() { + analogReadMilliVolts(NTC_PIN); // settle + uint32_t sum = 0; + for (int i = 0; i < 8; i++) { + uint32_t sample_mv = analogReadMilliVolts(NTC_PIN); + // GPIO14 is ADC2 on ESP32-S3. Wi-Fi/ESP-NOW arbitration failures are + // reported by Arduino as 0 mV; never average a failed sample into a + // plausible-but-low temperature. + if (sample_mv == 0 || sample_mv >= NTC_VCC_MV) return NAN; + sum += sample_mv; + } + float mv = sum / 8.0f; + + // R_ntc = R_fixed * (Vcc - V) / V for 3.3V-NTC-ADC-10k-GND + float r_ntc = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv; + if (r_ntc <= 0.0f) return NAN; + + float temp_k = 1.0f / (1.0f / 298.15f + (1.0f / NTC_B) * logf(r_ntc / NTC_R25)); + return temp_k - 273.15f; +} + +bool TBeam1WBoard::ntcImplausible(float temp_c) const { + return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; +} + +int TBeam1WBoard::rampDuty(float temp_c) const { + if (temp_c < (float)_lo_c) return 0; + if (temp_c >= (float)_hi_c) return 100; + float span = (float)(_hi_c - _lo_c); + if (span <= 0.0f) return 100; + float t = (temp_c - (float)_lo_c) / span; + return FAN_MIN_DUTY_PCT + (int)((100 - FAN_MIN_DUTY_PCT) * t + 0.5f); +} + +bool TBeam1WBoard::isTxCooling(uint32_t now) { + if (_tx_active) return true; + if (!_tx_cooldown_active) return false; + if ((int32_t)(now - _tx_until_ms) >= 0) { + _tx_cooldown_active = false; + return false; + } + return true; +} + +int TBeam1WBoard::cooldownSecs() { + if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; + if (!_tx_cooldown_active) return 0; + int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); + if (remain_ms <= 0) { + _tx_cooldown_active = false; + return 0; + } + return (remain_ms + 999) / 1000; +} + +void TBeam1WBoard::applyDuty(int pct) { + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + _duty_pct = pct; + + if (_drive == FAN_DRIVE_PWM) { + if (!_pwm_attached) { + ledcSetup(FAN_PWM_CHANNEL, FAN_PWM_FREQ_HZ, FAN_PWM_RES_BITS); + ledcAttachPin(FAN_CTRL_PIN, FAN_PWM_CHANNEL); + _pwm_attached = true; + } + uint32_t ticks = ((uint32_t)pct * FAN_PWM_MAX + 50) / 100; + ledcWrite(FAN_PWM_CHANNEL, ticks); + } else { + if (_pwm_attached) { + ledcDetachPin(FAN_CTRL_PIN); + _pwm_attached = false; + pinMode(FAN_CTRL_PIN, OUTPUT); + } + digitalWrite(FAN_CTRL_PIN, pct > 0 ? HIGH : LOW); + } +} + +void TBeam1WBoard::updateFan() { + float t = readNtcTempC(); + _temp_c = t; + bool tx_cooling = isTxCooling(millis()); + + int duty; + if (_manual_duty >= 0) { + duty = _manual_duty; + } else if (_mode == FAN_ON) { + duty = 100; + } else if (_mode == FAN_OFF) { + duty = 0; + } else if (ntcImplausible(t)) { + duty = 100; // fail-safe: treat bad NTC as hot + } else if (_drive == FAN_DRIVE_PWM) { + duty = rampDuty(t); + } else { + // Thermal hysteresis only: on at hi, off below lo. TX cooldown is applied + // after this and must not latch _thermal_on, or a TX at 30C keeps the fan + // running until temp dips under lo. + if (t >= (float)_hi_c) _thermal_on = true; + else if (t < (float)_lo_c) _thermal_on = false; + duty = _thermal_on ? 100 : 0; + } + + if (_mode == FAN_AUTO && _manual_duty < 0 && !ntcImplausible(t)) { + if (tx_cooling && duty < FAN_TX_FLOOR_PCT) duty = FAN_TX_FLOOR_PCT; + } + + applyDuty(duty); +} + +bool TBeam1WBoard::persistKey(const char* key, const char* value) { + return _prefs && _prefs->setByKey(key, value); +} + +bool TBeam1WBoard::parseIntArg(const char* text, int& value) { + if (!text || !*text) return false; + errno = 0; + char* end = nullptr; + long parsed = strtol(text, &end, 10); + if (errno == ERANGE || end == text || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + value = (int)parsed; + return true; +} + +void TBeam1WBoard::loadFanPrefs() { + if (!_prefs) return; + + char buf[12]; + buf[0] = 0; + if (_prefs->getByKey("fan", buf, 11)) { + if (strcmp(buf, "auto") == 0) _mode = FAN_AUTO; + else if (strcmp(buf, "off") == 0) _mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) _mode = FAN_ON; + } + + buf[0] = 0; + if (_prefs->getByKey("fan_drv", buf, 11)) { + if (strcmp(buf, "onoff") == 0) _drive = FAN_DRIVE_ONOFF; + else if (strcmp(buf, "pwm") == 0) _drive = FAN_DRIVE_PWM; + } + + int lo = _lo_c; + int hi = _hi_c; + buf[0] = 0; + if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); + buf[0] = 0; + if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + if (lo >= 0 && hi <= 120 && lo < hi) { + _lo_c = lo; + _hi_c = hi; + } + + _manual_duty = -1; +} + +void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + loadFanPrefs(); + updateFan(); +} + +const char* TBeam1WBoard::modeName() const { + if (_manual_duty >= 0) return "manual"; + if (_mode == FAN_AUTO) return "auto"; + if (_mode == FAN_OFF) return "off"; + return "on"; +} + +const char* TBeam1WBoard::driveName() const { + return _drive == FAN_DRIVE_ONOFF ? "onoff" : "pwm"; +} + +bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + (void)sender_timestamp; + + if (strcmp(command, "get fan") == 0) { + int cd = cooldownSecs(); + if (ntcImplausible(_temp_c)) { + sprintf(reply, "> %s n/a duty=%d%% %s cd=%ds", + modeName(), (int)_duty_pct, driveName(), cd); + } else { + sprintf(reply, "> %s %.1fC duty=%d%% %s cd=%ds", + modeName(), (double)_temp_c, (int)_duty_pct, driveName(), cd); + } + return true; + } + + if (strncmp(command, "set fan.lo ", 11) == 0) { + int lo; + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= _hi_c || lo > 100) { + strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); + } else if (!persistKey("fan_lo", &command[11])) { + strcpy(reply, "Error: failed to save fan.lo"); + } else { + _lo_c = lo; + sprintf(reply, "OK - fan.lo %d", lo); + } + return true; + } + + if (strncmp(command, "set fan.hi ", 11) == 0) { + int hi; + if (!parseIntArg(&command[11], hi) || hi <= _lo_c || hi > 120) { + strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); + } else if (!persistKey("fan_hi", &command[11])) { + strcpy(reply, "Error: failed to save fan.hi"); + } else { + _hi_c = hi; + sprintf(reply, "OK - fan.hi %d", hi); + } + return true; + } + + if (strncmp(command, "set fan.drive ", 14) == 0) { + const char* arg = &command[14]; + if (strcmp(arg, "pwm") == 0) { + if (!persistKey("fan_drv", "pwm")) { + strcpy(reply, "Error: failed to save fan.drive"); + } else { + _drive = FAN_DRIVE_PWM; + applyDuty(_duty_pct); + strcpy(reply, "OK - fan.drive pwm"); + } + } else if (strcmp(arg, "onoff") == 0) { + if (!persistKey("fan_drv", "onoff")) { + strcpy(reply, "Error: failed to save fan.drive"); + } else { + _drive = FAN_DRIVE_ONOFF; + applyDuty(_duty_pct); + strcpy(reply, "OK - fan.drive onoff"); + } + } else { + strcpy(reply, "Error: fan.drive must be pwm or onoff"); + } + return true; + } + + if (strncmp(command, "set fan.duty ", 13) == 0) { + int duty; + if (!parseIntArg(&command[13], duty) || duty < 0 || duty > 100) { + strcpy(reply, "Error: fan.duty must be 0-100"); + } else { + _manual_duty = duty; + applyDuty(duty); + sprintf(reply, "OK - fan.duty %d (not saved)", duty); + } + return true; + } + + if (strncmp(command, "set fan ", 8) == 0) { + const char* arg = &command[8]; + if (strcmp(arg, "on") == 0) { + if (!persistKey("fan", "on")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_ON; + _manual_duty = -1; + applyDuty(100); + strcpy(reply, "OK - fan on"); + } + } else if (strcmp(arg, "off") == 0) { + if (!persistKey("fan", "off")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_OFF; + _manual_duty = -1; + applyDuty(0); + strcpy(reply, "OK - fan off"); + } + } else if (strcmp(arg, "auto") == 0) { + if (!persistKey("fan", "auto")) { + strcpy(reply, "Error: failed to save fan mode"); + } else { + _mode = FAN_AUTO; + _manual_duty = -1; + updateFan(); + strcpy(reply, "OK - fan auto"); + } + } else { + strcpy(reply, "Error: fan must be on, off, or auto"); + } + return true; + } + + return false; } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index d999dfd4..e96923c5 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "variant.h" @@ -28,18 +30,53 @@ // - Battery must support 2A+ discharge for high-power TX class TBeam1WBoard : public ESP32Board { +public: + enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; + enum FanDrive { FAN_DRIVE_PWM, FAN_DRIVE_ONOFF }; + private: bool radio_powered = false; + bool _stopped = false; + bool _pwm_attached = false; + KeyValueStore* _prefs = nullptr; + FanMode _mode = FAN_AUTO; + FanDrive _drive = FAN_DRIVE_ONOFF; + int _lo_c = FAN_DEFAULT_LO_C; + int _hi_c = FAN_DEFAULT_HI_C; + int _manual_duty = -1; // -1 = follow mode; 0..100 = CLI override + bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this + volatile float _temp_c = NAN; + volatile int _duty_pct = 100; + volatile bool _tx_active = false; + volatile bool _tx_cooldown_active = false; + volatile uint32_t _tx_until_ms = 0; + TaskHandle_t _fan_task = nullptr; + + void startFanTask(); + void updateFan(); + void applyDuty(int pct); + float readNtcTempC(); + int rampDuty(float temp_c) const; + int cooldownSecs(); + bool isTxCooling(uint32_t now); + bool ntcImplausible(float temp_c) const; + bool persistKey(const char* key, const char* value); + static bool parseIntArg(const char* text, int& value); + void loadFanPrefs(); + const char* modeName() const; + const char* driveName() const; + static void fanTaskThunk(void* arg); public: void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override; void powerOff() override; - // Fan control methods void setFanEnabled(bool enabled); bool isFanEnabled() const; }; diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index f6807e56..a67e2e84 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -77,11 +77,26 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 3.0 -// NTC temperature sensor +// NTC thermistor (Murata NCP18XH103F03RB, 10k, B25/50=3380K) +// Divider: 3.3V -> NTC -> GPIO14 -> 10k pull-down -> GND (Vadc rises with temp) #define NTC_PIN 14 +#define NTC_B 3380.0f +#define NTC_R25 10000.0f +#define NTC_R_FIXED 10000.0f +#define NTC_VCC_MV 3300.0f -// Fan control +// Fan control (GPIO41). Default auto + on/off: NTC is PA-adjacent PCB temp, +// not die temp, so trip well below the SX1262/ESP32 85C operating limit. +// This fan/MOSFET path does not respond to PWM below 100% duty. #define FAN_CTRL_PIN 41 +#define FAN_PWM_CHANNEL 4 +#define FAN_PWM_FREQ_HZ 25000 +#define FAN_PWM_RES_BITS 8 +#define FAN_MIN_DUTY_PCT 40 +#define FAN_TX_COOLDOWN_MS 15000 +#define FAN_TX_FLOOR_PCT 100 +#define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) +#define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U From 6c1f4c09a6e46603ee291e36bb8d8f6446e75b33 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 4 Sep 2026 16:51:53 -0700 Subject: [PATCH 084/174] Fix T-Beam 1W fan races and remove PWM controls --- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 222 +++++++++------------- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 25 +-- variants/lilygo_tbeam_1w/variant.h | 11 +- 3 files changed, 102 insertions(+), 156 deletions(-) diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index e82e4623..e0419955 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -5,8 +5,6 @@ #include #include -static const int FAN_PWM_MAX = (1 << FAN_PWM_RES_BITS) - 1; - void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -30,8 +28,8 @@ void TBeam1WBoard::begin() { // Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown. pinMode(FAN_CTRL_PIN, OUTPUT); digitalWrite(FAN_CTRL_PIN, HIGH); + _fan_on = true; _temp_c = readNtcTempC(); - applyDuty(100); startFanTask(); } @@ -43,26 +41,30 @@ void TBeam1WBoard::startFanTask() { void TBeam1WBoard::fanTaskThunk(void* arg) { auto* self = static_cast(arg); for (;;) { - if (!self->_stopped) { - self->updateFan(); - } + self->updateFan(); vTaskDelay(pdMS_TO_TICKS(1000)); } } void TBeam1WBoard::onBeforeTransmit() { digitalWrite(LED_PIN, HIGH); // TX LED on + portENTER_CRITICAL(&_fan_mux); _tx_active = true; - if (_mode == FAN_AUTO && _manual_duty < 0 && !_stopped) { - applyDuty(FAN_TX_FLOOR_PCT); + if (_mode == FAN_AUTO && !_stopped) { + setFanOutputLocked(true); } + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::onAfterTransmit() { digitalWrite(LED_PIN, LOW); // TX LED off - _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; - _tx_cooldown_active = true; + portENTER_CRITICAL(&_fan_mux); + if (!_stopped) { + _tx_until_ms = millis() + FAN_TX_COOLDOWN_MS; + _tx_cooldown_active = true; + } _tx_active = false; + portEXIT_CRITICAL(&_fan_mux); } uint16_t TBeam1WBoard::getBattMilliVolts() { @@ -81,8 +83,12 @@ const char* TBeam1WBoard::getManufacturerName() const { } void TBeam1WBoard::powerOff() { + portENTER_CRITICAL(&_fan_mux); _stopped = true; - applyDuty(0); + _tx_active = false; + _tx_cooldown_active = false; + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); // Turn off radio LNA (CTRL pin must be LOW when not receiving) digitalWrite(SX126X_RXEN, LOW); @@ -97,11 +103,16 @@ void TBeam1WBoard::powerOff() { } void TBeam1WBoard::setFanEnabled(bool enabled) { - applyDuty(enabled ? 100 : 0); + portENTER_CRITICAL(&_fan_mux); + setFanOutputLocked(enabled && !_stopped); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::isFanEnabled() const { - return _duty_pct > 0; + portENTER_CRITICAL(&_fan_mux); + bool enabled = _fan_on; + portEXIT_CRITICAL(&_fan_mux); + return enabled; } float TBeam1WBoard::readNtcTempC() { @@ -129,16 +140,7 @@ bool TBeam1WBoard::ntcImplausible(float temp_c) const { return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f; } -int TBeam1WBoard::rampDuty(float temp_c) const { - if (temp_c < (float)_lo_c) return 0; - if (temp_c >= (float)_hi_c) return 100; - float span = (float)(_hi_c - _lo_c); - if (span <= 0.0f) return 100; - float t = (temp_c - (float)_lo_c) / span; - return FAN_MIN_DUTY_PCT + (int)((100 - FAN_MIN_DUTY_PCT) * t + 0.5f); -} - -bool TBeam1WBoard::isTxCooling(uint32_t now) { +bool TBeam1WBoard::isTxCoolingLocked(uint32_t now) { if (_tx_active) return true; if (!_tx_cooldown_active) return false; if ((int32_t)(now - _tx_until_ms) >= 0) { @@ -148,7 +150,7 @@ bool TBeam1WBoard::isTxCooling(uint32_t now) { return true; } -int TBeam1WBoard::cooldownSecs() { +int TBeam1WBoard::cooldownSecsLocked() { if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000; if (!_tx_cooldown_active) return 0; int32_t remain_ms = (int32_t)(_tx_until_ms - millis()); @@ -159,59 +161,41 @@ int TBeam1WBoard::cooldownSecs() { return (remain_ms + 999) / 1000; } -void TBeam1WBoard::applyDuty(int pct) { - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - _duty_pct = pct; - - if (_drive == FAN_DRIVE_PWM) { - if (!_pwm_attached) { - ledcSetup(FAN_PWM_CHANNEL, FAN_PWM_FREQ_HZ, FAN_PWM_RES_BITS); - ledcAttachPin(FAN_CTRL_PIN, FAN_PWM_CHANNEL); - _pwm_attached = true; - } - uint32_t ticks = ((uint32_t)pct * FAN_PWM_MAX + 50) / 100; - ledcWrite(FAN_PWM_CHANNEL, ticks); - } else { - if (_pwm_attached) { - ledcDetachPin(FAN_CTRL_PIN); - _pwm_attached = false; - pinMode(FAN_CTRL_PIN, OUTPUT); - } - digitalWrite(FAN_CTRL_PIN, pct > 0 ? HIGH : LOW); - } +void TBeam1WBoard::setFanOutputLocked(bool enabled) { + _fan_on = enabled; + digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); } void TBeam1WBoard::updateFan() { float t = readNtcTempC(); - _temp_c = t; - bool tx_cooling = isTxCooling(millis()); + uint32_t now = millis(); - int duty; - if (_manual_duty >= 0) { - duty = _manual_duty; - } else if (_mode == FAN_ON) { - duty = 100; + portENTER_CRITICAL(&_fan_mux); + if (_stopped) { + portEXIT_CRITICAL(&_fan_mux); + return; + } + + _temp_c = t; + bool tx_cooling = isTxCoolingLocked(now); + + bool enabled; + if (_mode == FAN_ON) { + enabled = true; } else if (_mode == FAN_OFF) { - duty = 0; + enabled = false; } else if (ntcImplausible(t)) { - duty = 100; // fail-safe: treat bad NTC as hot - } else if (_drive == FAN_DRIVE_PWM) { - duty = rampDuty(t); + enabled = true; // fail-safe: treat bad NTC as hot } else { - // Thermal hysteresis only: on at hi, off below lo. TX cooldown is applied - // after this and must not latch _thermal_on, or a TX at 30C keeps the fan - // running until temp dips under lo. + // TX cooldown must not latch _thermal_on, or a TX at 30C keeps the fan + // running until the temperature dips under lo. if (t >= (float)_hi_c) _thermal_on = true; else if (t < (float)_lo_c) _thermal_on = false; - duty = _thermal_on ? 100 : 0; + enabled = _thermal_on || tx_cooling; } - if (_mode == FAN_AUTO && _manual_duty < 0 && !ntcImplausible(t)) { - if (tx_cooling && duty < FAN_TX_FLOOR_PCT) duty = FAN_TX_FLOOR_PCT; - } - - applyDuty(duty); + setFanOutputLocked(enabled); + portEXIT_CRITICAL(&_fan_mux); } bool TBeam1WBoard::persistKey(const char* key, const char* value) { @@ -233,32 +217,28 @@ bool TBeam1WBoard::parseIntArg(const char* text, int& value) { void TBeam1WBoard::loadFanPrefs() { if (!_prefs) return; + FanMode mode = FAN_AUTO; char buf[12]; buf[0] = 0; if (_prefs->getByKey("fan", buf, 11)) { - if (strcmp(buf, "auto") == 0) _mode = FAN_AUTO; - else if (strcmp(buf, "off") == 0) _mode = FAN_OFF; - else if (strcmp(buf, "on") == 0) _mode = FAN_ON; + if (strcmp(buf, "off") == 0) mode = FAN_OFF; + else if (strcmp(buf, "on") == 0) mode = FAN_ON; } - buf[0] = 0; - if (_prefs->getByKey("fan_drv", buf, 11)) { - if (strcmp(buf, "onoff") == 0) _drive = FAN_DRIVE_ONOFF; - else if (strcmp(buf, "pwm") == 0) _drive = FAN_DRIVE_PWM; - } - - int lo = _lo_c; - int hi = _hi_c; + int lo = FAN_DEFAULT_LO_C; + int hi = FAN_DEFAULT_HI_C; buf[0] = 0; if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf); buf[0] = 0; if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf); + + portENTER_CRITICAL(&_fan_mux); + _mode = mode; if (lo >= 0 && hi <= 120 && lo < hi) { _lo_c = lo; _hi_c = hi; } - - _manual_duty = -1; + portEXIT_CRITICAL(&_fan_mux); } void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { @@ -267,40 +247,44 @@ void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) { updateFan(); } -const char* TBeam1WBoard::modeName() const { - if (_manual_duty >= 0) return "manual"; +const char* TBeam1WBoard::modeNameLocked() const { if (_mode == FAN_AUTO) return "auto"; if (_mode == FAN_OFF) return "off"; return "on"; } -const char* TBeam1WBoard::driveName() const { - return _drive == FAN_DRIVE_ONOFF ? "onoff" : "pwm"; -} - bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { (void)sender_timestamp; if (strcmp(command, "get fan") == 0) { - int cd = cooldownSecs(); - if (ntcImplausible(_temp_c)) { - sprintf(reply, "> %s n/a duty=%d%% %s cd=%ds", - modeName(), (int)_duty_pct, driveName(), cd); + portENTER_CRITICAL(&_fan_mux); + int cd = cooldownSecsLocked(); + float temp_c = _temp_c; + bool enabled = _fan_on; + const char* mode = modeNameLocked(); + portEXIT_CRITICAL(&_fan_mux); + if (ntcImplausible(temp_c)) { + sprintf(reply, "> %s n/a fan=%s cd=%ds", mode, enabled ? "on" : "off", cd); } else { - sprintf(reply, "> %s %.1fC duty=%d%% %s cd=%ds", - modeName(), (double)_temp_c, (int)_duty_pct, driveName(), cd); + sprintf(reply, "> %s %.1fC fan=%s cd=%ds", + mode, (double)temp_c, enabled ? "on" : "off", cd); } return true; } if (strncmp(command, "set fan.lo ", 11) == 0) { int lo; - if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= _hi_c || lo > 100) { + portENTER_CRITICAL(&_fan_mux); + int hi_limit = _hi_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= hi_limit || lo > 100) { strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi"); } else if (!persistKey("fan_lo", &command[11])) { strcpy(reply, "Error: failed to save fan.lo"); } else { + portENTER_CRITICAL(&_fan_mux); _lo_c = lo; + portEXIT_CRITICAL(&_fan_mux); sprintf(reply, "OK - fan.lo %d", lo); } return true; @@ -308,79 +292,51 @@ bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, if (strncmp(command, "set fan.hi ", 11) == 0) { int hi; - if (!parseIntArg(&command[11], hi) || hi <= _lo_c || hi > 120) { + portENTER_CRITICAL(&_fan_mux); + int lo_limit = _lo_c; + portEXIT_CRITICAL(&_fan_mux); + if (!parseIntArg(&command[11], hi) || hi <= lo_limit || hi > 120) { strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120"); } else if (!persistKey("fan_hi", &command[11])) { strcpy(reply, "Error: failed to save fan.hi"); } else { + portENTER_CRITICAL(&_fan_mux); _hi_c = hi; + portEXIT_CRITICAL(&_fan_mux); sprintf(reply, "OK - fan.hi %d", hi); } return true; } - if (strncmp(command, "set fan.drive ", 14) == 0) { - const char* arg = &command[14]; - if (strcmp(arg, "pwm") == 0) { - if (!persistKey("fan_drv", "pwm")) { - strcpy(reply, "Error: failed to save fan.drive"); - } else { - _drive = FAN_DRIVE_PWM; - applyDuty(_duty_pct); - strcpy(reply, "OK - fan.drive pwm"); - } - } else if (strcmp(arg, "onoff") == 0) { - if (!persistKey("fan_drv", "onoff")) { - strcpy(reply, "Error: failed to save fan.drive"); - } else { - _drive = FAN_DRIVE_ONOFF; - applyDuty(_duty_pct); - strcpy(reply, "OK - fan.drive onoff"); - } - } else { - strcpy(reply, "Error: fan.drive must be pwm or onoff"); - } - return true; - } - - if (strncmp(command, "set fan.duty ", 13) == 0) { - int duty; - if (!parseIntArg(&command[13], duty) || duty < 0 || duty > 100) { - strcpy(reply, "Error: fan.duty must be 0-100"); - } else { - _manual_duty = duty; - applyDuty(duty); - sprintf(reply, "OK - fan.duty %d (not saved)", duty); - } - return true; - } - if (strncmp(command, "set fan ", 8) == 0) { const char* arg = &command[8]; if (strcmp(arg, "on") == 0) { if (!persistKey("fan", "on")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_ON; - _manual_duty = -1; - applyDuty(100); + if (!_stopped) setFanOutputLocked(true); + portEXIT_CRITICAL(&_fan_mux); strcpy(reply, "OK - fan on"); } } else if (strcmp(arg, "off") == 0) { if (!persistKey("fan", "off")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_OFF; - _manual_duty = -1; - applyDuty(0); + setFanOutputLocked(false); + portEXIT_CRITICAL(&_fan_mux); strcpy(reply, "OK - fan off"); } } else if (strcmp(arg, "auto") == 0) { if (!persistKey("fan", "auto")) { strcpy(reply, "Error: failed to save fan mode"); } else { + portENTER_CRITICAL(&_fan_mux); _mode = FAN_AUTO; - _manual_duty = -1; + portEXIT_CRITICAL(&_fan_mux); updateFan(); strcpy(reply, "OK - fan auto"); } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index e96923c5..429b7bd1 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -32,39 +32,34 @@ class TBeam1WBoard : public ESP32Board { public: enum FanMode { FAN_ON, FAN_OFF, FAN_AUTO }; - enum FanDrive { FAN_DRIVE_PWM, FAN_DRIVE_ONOFF }; private: bool radio_powered = false; bool _stopped = false; - bool _pwm_attached = false; KeyValueStore* _prefs = nullptr; FanMode _mode = FAN_AUTO; - FanDrive _drive = FAN_DRIVE_ONOFF; int _lo_c = FAN_DEFAULT_LO_C; int _hi_c = FAN_DEFAULT_HI_C; - int _manual_duty = -1; // -1 = follow mode; 0..100 = CLI override bool _thermal_on = false; // onoff hysteresis; TX boost must not latch this - volatile float _temp_c = NAN; - volatile int _duty_pct = 100; - volatile bool _tx_active = false; - volatile bool _tx_cooldown_active = false; - volatile uint32_t _tx_until_ms = 0; + bool _fan_on = true; + float _temp_c = NAN; + bool _tx_active = false; + bool _tx_cooldown_active = false; + uint32_t _tx_until_ms = 0; TaskHandle_t _fan_task = nullptr; + mutable portMUX_TYPE _fan_mux = portMUX_INITIALIZER_UNLOCKED; void startFanTask(); void updateFan(); - void applyDuty(int pct); + void setFanOutputLocked(bool enabled); float readNtcTempC(); - int rampDuty(float temp_c) const; - int cooldownSecs(); - bool isTxCooling(uint32_t now); + int cooldownSecsLocked(); + bool isTxCoolingLocked(uint32_t now); bool ntcImplausible(float temp_c) const; bool persistKey(const char* key, const char* value); static bool parseIntArg(const char* text, int& value); void loadFanPrefs(); - const char* modeName() const; - const char* driveName() const; + const char* modeNameLocked() const; static void fanTaskThunk(void* arg); public: diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index a67e2e84..528459a4 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -85,16 +85,11 @@ #define NTC_R_FIXED 10000.0f #define NTC_VCC_MV 3300.0f -// Fan control (GPIO41). Default auto + on/off: NTC is PA-adjacent PCB temp, -// not die temp, so trip well below the SX1262/ESP32 85C operating limit. -// This fan/MOSFET path does not respond to PWM below 100% duty. +// Fan control (GPIO41). NTC is PA-adjacent PCB temp, not die temp, so trip +// well below the SX1262/ESP32 85C operating limit. Hardware testing confirmed +// that this fan/MOSFET path is on/off; PWM below 100% does not spin the fan. #define FAN_CTRL_PIN 41 -#define FAN_PWM_CHANNEL 4 -#define FAN_PWM_FREQ_HZ 25000 -#define FAN_PWM_RES_BITS 8 -#define FAN_MIN_DUTY_PCT 40 #define FAN_TX_COOLDOWN_MS 15000 -#define FAN_TX_FLOOR_PCT 100 #define FAN_DEFAULT_LO_C 30 // off below typical indoor idle (~86F) #define FAN_DEFAULT_HI_C 36 // on at ~97F PCB; still far below 85C chip ratings From 62d0556a7024dd86046cea9d74e176f4737d91a0 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 5 Sep 2026 15:59:29 +1000 Subject: [PATCH 085/174] * added region load/save support * SUBSCRIBE now with timeout param (default 30 mins, max ~18 hours) --- examples/simple_sensor/SensorMesh.cpp | 102 ++++++++++++++++++++++---- examples/simple_sensor/SensorMesh.h | 7 +- src/helpers/ClientACL.h | 1 + 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 008cbcfc..c3f2d9b0 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -322,8 +322,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 2 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - uint8_t reserved = payload[0]; + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + uint16_t timeout_secs; + memcpy(&timeout_secs, &payload[0], 2); + uint8_t reserved = payload[2]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -331,16 +333,19 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u r = region_map.getDefaultRegion(); } from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.min_deltas_len = payload[1]; + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + from->extra.sensor.min_deltas_len = payload[3]; // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[2], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[1])); + memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); - getRNG()->random(&reply_data[4], 2); // just some entropy for better packet-hash uniqueness - strcpy((char *)&reply_data[6], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[6]); + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + return 6 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.expiry_timestamp = 0; + // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness return 8; @@ -441,6 +446,25 @@ int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } +void SensorMesh::startRegionsLoad() { + temp_map.resetFrom(region_map); // rebuild regions in a temp instance + memset(load_stack, 0, sizeof(load_stack)); + load_stack[0] = &temp_map.getWildcard(); + region_load_active = true; +} + +bool SensorMesh::saveRegions() { + return region_map.save(_fs); +} + +void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) { + if (r) { + region_map.getTransportKeysFor(*r, &default_scope, 1); + } else { + memset(default_scope.key, 0, sizeof(default_scope.key)); + } +} + uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { ClientInfo* client; if (data[0] == 0) { // blank password, just check if sender is in ACL @@ -454,7 +478,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } else { if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password #if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]); #endif return 0; } @@ -491,6 +515,40 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) { + if (region_load_active) { + if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation + region_map = temp_map; // copy over the temp instance as new current map + region_load_active = false; + + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); + } else { + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; + + char *ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name + + while (*ep && *ep != 'F') ep++; // look for (optional) flags + + if (indent > 0 && indent < 8 && strlen(np) > 0) { + auto parent = load_stack[indent - 1]; + if (parent) { + auto old = region_map.findByName(np); + auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists) + if (nw) { + nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr + + load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's + } + } + } + reply[0] = 0; + } + return; + } + while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -548,15 +606,25 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char } else { // use default scope r = region_map.getDefaultRegion(); } - if (command[3] == ' ') { - // compile params as LPP data, eg. "sub 1:0.2V" - from->extra.sensor.min_deltas_len = compileLPPSpec(&command[4], from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } else { - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + // defaults: + from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) + uint16_t timeout_secs = 30*60; // expires after 30 mins + if (command[3] == ' ') { // eg. "sub 300 1:0.2V" + char* cp = &command[4]; + while (*cp >= '0' && *cp <= '9') cp++; + if (cp > &command[4]) { + timeout_secs = atoi(&command[4]); + if (*cp == ' ') { + cp++; // skip the space + from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); + } + } } from->extra.sensor.scope_region_id = r ? r->id : 0; - if (from->extra.sensor.scope_region_id) { - strcpy(reply, "OK - subscribed"); + from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; + if (from->extra.sensor.expiry_timestamp) { + DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); + sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); } else { strcpy(reply, "Err - region scope needed"); } @@ -853,7 +921,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - region_map(key_store), + region_map(key_store), temp_map(key_store), _cli(board, rtc, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { @@ -864,6 +932,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; prev_telem_size = 0; + region_load_active = false; // defaults _prefs.airtime_factor = 1.0; @@ -1110,6 +1179,7 @@ void SensorMesh::loop() { if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope + if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6d5857d0..e0e03c81 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -78,6 +78,9 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override { } void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + void startRegionsLoad() override; + bool saveRegions() override; + void onDefaultRegionChanged(const RegionEntry* r) override; float getTelemValue(uint8_t channel, uint8_t type); @@ -148,8 +151,9 @@ private: uint8_t prev_telem_size; uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; - RegionMap region_map; + RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; + RegionEntry* load_stack[8]; TransportKey default_scope; uint32_t last_read_time; int matching_peer_indexes[MAX_SEARCH_RESULTS]; @@ -160,6 +164,7 @@ private: float pending_bw; uint8_t pending_sf; uint8_t pending_cr; + bool region_load_active; bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 9fde35ec..1e9f472d 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -29,6 +29,7 @@ struct ClientInfo { uint8_t push_failures; } room; struct { + uint32_t expiry_timestamp; // epoch seconds uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From 0b2c47a932763daa06c395fe81391d59e95141f0 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:10:20 -0500 Subject: [PATCH 086/174] Add WiFi companion support for Pico W SerialWifiInterface has no ESP32-specific code, so move it to helpers/wifi and reuse it on RP2040. Guard the ESP32-only WiFi event/auto-reconnect calls and poll link state on RP2040 instead. --- examples/companion_radio/main.cpp | 24 ++++++++++----- platformio.ini | 1 + .../{esp32 => wifi}/SerialWifiInterface.cpp | 0 .../{esp32 => wifi}/SerialWifiInterface.h | 0 variants/rpi_picow/platformio.ini | 30 ++++++++++--------- 5 files changed, 34 insertions(+), 21 deletions(-) rename src/helpers/{esp32 => wifi}/SerialWifiInterface.cpp (100%) rename src/helpers/{esp32 => wifi}/SerialWifiInterface.h (100%) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 89f0e6cb..7c8c12b9 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,9 +36,8 @@ MultiSerialInterface interface_manager; #ifndef TCP_PORT #define TCP_PORT 5000 #endif - #ifdef ESP32 - // include esp32 wifi interface - #include + #if defined(ESP32) || defined(RP2040_PLATFORM) + #include SerialWifiInterface wifi_interface; #else #error "SerialWifiInterface is not defined for this platform" @@ -108,7 +107,7 @@ void halt() { } /* WIFI RECONNECT TRACKERS */ -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; #endif @@ -192,6 +191,7 @@ void setup() { // add wifi interface #ifdef WIFI_SSID +#if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -204,6 +204,7 @@ void setup() { wifi_needs_reconnect = false; } }); +#endif WiFi.begin(WIFI_SSID, WIFI_PWD); wifi_interface.begin(TCP_PORT); @@ -262,12 +263,21 @@ void loop() { #endif } -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef WIFI_SSID + // RP2040 has no WiFi event callbacks, so poll the link state instead + #if defined(RP2040_PLATFORM) + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + #endif + // Safely attempt to reconnect every 10 seconds if flagged if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); - WiFi.disconnect(); - WiFi.reconnect(); + #if defined(RP2040_PLATFORM) + WiFi.begin(WIFI_SSID, WIFI_PWD); // no reconnect() on this platform + #else + WiFi.disconnect(); + WiFi.reconnect(); + #endif last_wifi_reconnect_attempt = millis(); } #endif diff --git a/platformio.ini b/platformio.ini index 2219c978..622b01e2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,6 +64,7 @@ build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} + + [esp32_ota] lib_deps = diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/wifi/SerialWifiInterface.cpp similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.cpp rename to src/helpers/wifi/SerialWifiInterface.cpp diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/wifi/SerialWifiInterface.h similarity index 100% rename from src/helpers/esp32/SerialWifiInterface.h rename to src/helpers/wifi/SerialWifiInterface.h diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 0fe8c436..32944a9a 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -81,20 +81,22 @@ lib_ignore = BLE ; lib_deps = ${rpi_picow.lib_deps} ; densaugeo/base64 @ ~1.4.0 -; [env:PicoW_companion_radio_wifi] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D WIFI_DEBUG_LOGGING=1 -; -D WIFI_SSID='"myssid"' -; -D WIFI_PWD='"mypwd"' -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_wifi] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 +lib_ignore = BLE [env:PicoW_terminal_chat] extends = rpi_picow From 739a67c9f1504e029272a2fc68d241f1e9107846 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:18:51 -0500 Subject: [PATCH 087/174] Allow WiFi credentials to be set at runtime Store ssid/pwd in NodePrefs and set them with 'set wifi.ssid' / 'set wifi.pwd' over USB serial; build-time WIFI_SSID/WIFI_PWD stay as the fallback. Headless WiFi builds get the config CLI on Serial, which is otherwise unused there. --- examples/companion_radio/MyMesh.cpp | 34 ++++++++++++++++++++++++++++ examples/companion_radio/NodePrefs.h | 27 +++++++++++++++++++++- examples/companion_radio/main.cpp | 13 +++++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ee8114ca..8550890e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -932,6 +932,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { _iter_started = false; _cli_rescue = false; + cli_command[0] = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -2156,6 +2157,35 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } +#ifdef WIFI_SSID + // local console only: these are credentials, and remote admin has no business with them + if (sender_timestamp == 0) { + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); + savePrefs(); + sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); + savePrefs(); + strcpy(reply, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "set wifi.clear") == 0) { + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + savePrefs(); + strcpy(reply, "> wifi config cleared, using build-time credentials (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "(build-time)"); + return true; + } + } +#endif + if (strcmp(command, "board") == 0) { strcpy(reply, board.getManufacturerName()); return true; @@ -2386,6 +2416,10 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); +#if defined(WIFI_SSID) && !defined(ENABLE_USB_INTERFACE) + // headless WiFi build: USB serial isn't a companion transport, so use it for config + checkCLIRescueCmd(); +#endif } // is there are pending dirty contacts write needed? diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f6f9b887..c725c317 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -44,6 +44,10 @@ public: char default_scope_name[31]; uint8_t default_scope_key[16]; int8_t tz_offset = 0; +#ifdef WIFI_SSID + char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used + char wifi_pwd[64] = {0}; +#endif private: class RadioPrefs : public CommonRadioPrefs { @@ -160,6 +164,20 @@ private: DynamicConfigSerializer custom; +#ifdef WIFI_SSID + class WiFiPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid)); + def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd)); + } + public: + WiFiPrefs(NodePrefs* parent) : _parent(parent) { } + }; + WiFiPrefs wifi; +#endif + protected: void structure() override { def("name", node_name, sizeof(node_name)); @@ -172,9 +190,16 @@ protected: def("repeat", repeat); def("comp", companion); def("custom", custom); +#ifdef WIFI_SSID + def("wifi", wifi); +#endif } public: - NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) { + NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) +#ifdef WIFI_SSID + , wifi(this) +#endif + { node_name[0] = 0; default_scope_name[0] = 0; memset(default_scope_key, 0, sizeof(default_scope_key)); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 7c8c12b9..64aa3d26 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -110,6 +110,8 @@ void halt() { #ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; + const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set + const char* wifi_pwd = WIFI_PWD; #endif void setup() { @@ -206,7 +208,14 @@ void setup() { }); #endif - WiFi.begin(WIFI_SSID, WIFI_PWD); + // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial) + if (the_mesh.getNodePrefs()->wifi_ssid[0]) { + wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; + wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; + } + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + + WiFi.begin(wifi_ssid, wifi_pwd); wifi_interface.begin(TCP_PORT); interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif @@ -273,7 +282,7 @@ void loop() { if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); #if defined(RP2040_PLATFORM) - WiFi.begin(WIFI_SSID, WIFI_PWD); // no reconnect() on this platform + WiFi.begin(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); WiFi.reconnect(); From 58181bb8a285d7c1658dc0c4908232440ebdf829 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:33:40 -0500 Subject: [PATCH 088/174] Fix blocking WiFi connect on RP2040, log link state arduino-pico's WiFi.begin() blocks for up to 2x its 15s timeout, which stalled the mesh loop on every reconnect attempt; use beginNoBlock(). Log the IP when the link comes up, and the status code when retrying. --- examples/companion_radio/main.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 64aa3d26..f0c12908 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -112,6 +112,7 @@ void halt() { unsigned long last_wifi_reconnect_attempt = 0; const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set const char* wifi_pwd = WIFI_PWD; + bool wifi_was_connected = false; #endif void setup() { @@ -215,7 +216,11 @@ void setup() { } WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); +#if defined(RP2040_PLATFORM) + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // begin() blocks for up to 2x its 15s timeout +#else WiFi.begin(wifi_ssid, wifi_pwd); +#endif wifi_interface.begin(TCP_PORT); interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif @@ -276,13 +281,21 @@ void loop() { // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + wifi_was_connected = !wifi_needs_reconnect; + if (wifi_was_connected) { + WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); + } else { + WIFI_DEBUG_PRINTLN("link lost"); + } + } #endif // Safely attempt to reconnect every 10 seconds if flagged if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) - WiFi.begin(wifi_ssid, wifi_pwd); // no reconnect() on this platform + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); WiFi.reconnect(); From a4ac85a0d6b3d392f303e054ab03516933dbec52 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Fri, 4 Sep 2026 20:57:01 -0500 Subject: [PATCH 089/174] Address review findings on WiFi companion support - scope the serial config CLI to RP2040; it was exposing the rescue CLI (cat/rm/erase) on every ESP32 WiFi build, which gates it behind a physical long-press - bound and space out RP2040 rejoins: the core's join busy-waits, so cap it at 5s and retry every 30s instead of every 10s - stamp the reconnect timer in setup(), so the first loop() doesn't tear down an association that is still finishing DHCP - treat stored credentials as a pair, and pass NULL (not "") for an open network - teach build_as_lib.py where SerialWifiInterface moved --- build_as_lib.py | 2 ++ examples/companion_radio/MyMesh.cpp | 5 +++-- examples/companion_radio/main.cpp | 24 ++++++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/build_as_lib.py b/build_as_lib.py index d8e95378..fbc7c15a 100644 --- a/build_as_lib.py +++ b/build_as_lib.py @@ -20,10 +20,12 @@ for item in menv.get("CPPDEFINES", []): src_filter.append("+") elif item == "ESP32": src_filter.append("+") + src_filter.append("+") elif item == "NRF52_PLATFORM": src_filter.append("+") elif item == "RP2040_PLATFORM": src_filter.append("+") + src_filter.append("+") # DISPLAY HANDLING elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS": diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 8550890e..a8e305a6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2416,8 +2416,9 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); -#if defined(WIFI_SSID) && !defined(ENABLE_USB_INTERFACE) - // headless WiFi build: USB serial isn't a companion transport, so use it for config +#if defined(WIFI_SSID) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) + // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a + // display + long-press), so serve config commands on the otherwise unused USB serial checkCLIRescueCmd(); #endif } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index f0c12908..ff0794ab 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,6 +36,12 @@ MultiSerialInterface interface_manager; #ifndef TCP_PORT #define TCP_PORT 5000 #endif + #ifndef WIFI_RETRY_INTERVAL + #define WIFI_RETRY_INTERVAL 30000 // millis between reconnect attempts + #endif + #ifndef WIFI_RETRY_TIMEOUT + #define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop() + #endif #if defined(ESP32) || defined(RP2040_PLATFORM) #include SerialWifiInterface wifi_interface; @@ -209,15 +215,23 @@ void setup() { }); #endif - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial) + // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). + // they are taken as a pair, so 'set wifi.ssid' alone gives an open-network join, not a + // silent fallback to the build-time password of a different network. if (the_mesh.getNodePrefs()->wifi_ssid[0]) { wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; } + if (wifi_pwd[0] == 0) wifi_pwd = NULL; // NULL (not "") selects an open network WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // begin() blocks for up to 2x its 15s timeout + // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the + // extra DHCP wait. Give the first connect a full window, then bound the retries below. + // Upgrade path if the stall ever matters: run WiFi on core1. + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); + last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else WiFi.begin(wifi_ssid, wifi_pwd); #endif @@ -291,10 +305,12 @@ void loop() { } #endif - // Safely attempt to reconnect every 10 seconds if flagged - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { + // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop + // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else WiFi.disconnect(); From d33fb4e9a7f61b0d25d30a2c78feed05dee42496 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sat, 5 Sep 2026 21:15:12 -0500 Subject: [PATCH 090/174] Fix config parser aborting on an empty object 'custom:{}' (a DynamicConfigSerializer with nothing set) hits EXPECT_KEY with a '}' and returns TOK_ERROR, so loadSerial stops there and silently drops every property after it. Nothing follows 'custom' in NodePrefs today, so it goes unnoticed until you add one. Also include stdlib.h, which Arduino.h was providing on-device but not in the native test build. --- src/helpers/ConfigSerializer.cpp | 2 + .../test_config_serializer.cpp | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index adff147f..36aff5cc 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,4 +1,5 @@ #include "ConfigSerializer.h" +#include // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not) bool ConfigSerializer::saveSerial(Stream& s) { Context context(&s, OP::WRITE); @@ -62,6 +63,7 @@ int ConfigSerializer::Context::readNext() { case EXPECT_COMMA_OR_KEY: if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } case EXPECT_KEY: + if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}' if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index dec55483..80d5e780 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -185,6 +185,47 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } +class TestNested : public ConfigSerializer { + class Inner : public ConfigSerializer { + protected: + void structure() override { } // no properties, so it writes as '{}' + }; + Inner inner; + protected: + void structure() override { + def("age", age); + def("inner", inner); + def("name", name, sizeof(name)); // comes *after* the empty sub-object + } + public: + int32_t age; + char name[16]; +}; + +TEST(ConfigSerializer, LoadSerial_EmptyObject) { + MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); // properties after an empty object must still load +} + +TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) { + MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}"); + TestNested data; + data.name[0] = 0; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + TEST(DynamicConfigSerializer, GetSet_Basic) { DynamicConfigSerializer data; From 5d82ed352c2481f5ec36e4b8e8ab57c963cb6c20 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Sun, 6 Sep 2026 15:14:35 +0200 Subject: [PATCH 091/174] Companion CLI: fixed txdelay, direct.txdelay, agc.reset.interval, int.thresh --- examples/companion_radio/MyMesh.cpp | 10 +++++++--- examples/companion_radio/MyMesh.h | 3 +++ examples/companion_radio/NodePrefs.h | 28 ++++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f5a4a5a9..53e6f4d5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -261,7 +261,7 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return _prefs.interference_threshold; } bool MyMesh::getCADEnabled() const { return _prefs.cad_enabled; @@ -273,11 +273,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } @@ -890,6 +890,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe // defaults _prefs.airtime_factor = 1.0; + _prefs.tx_delay_factor = 0.5f; + _prefs.direct_tx_delay_factor = 0.2f; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; _prefs.sf = LORA_SF; @@ -952,6 +954,8 @@ void MyMesh::begin(bool has_display) { // sanitise bad pref values _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + _prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f); + _prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f); _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); _prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f); _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 780de35d..4aab11cd 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -106,6 +106,9 @@ protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; bool getCADEnabled() const override; + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } int calcRxDelay(float score, uint32_t air_time) const override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c8a9ab83..4581185e 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -27,6 +27,8 @@ public: uint8_t telemetry_mode_loc = 0; uint8_t telemetry_mode_env = 0; float rx_delay_base = 0; + float tx_delay_factor = 0; + float direct_tx_delay_factor = 0; uint32_t ble_pin = 0; uint8_t advert_loc_policy = 0; uint8_t buzzer_quiet = 0; @@ -41,6 +43,8 @@ public: uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) uint8_t cad_enabled = 0; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // secs / 4 char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -54,16 +58,16 @@ private: def("sf", _parent->sf); def("cr", _parent->cr); def("cad", _parent->cad_enabled); - //def("int_thr", _parent->interference_threshold); + def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously def("fem_txgain", _parent->radio_fem_txgain); def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); - //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded - //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded - //def("agc_int", _parent->agc_reset_interval); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); def("hash_mode", _parent->path_hash_mode); def("multi_ack", _parent->multi_acks); } @@ -83,24 +87,24 @@ private: void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } - uint8_t getIntThresh() const override { return 0; } - void setIntThresh(uint8_t t) override { /* no-op */ } + uint8_t getIntThresh() const override { return _parent->interference_threshold; } + void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } uint8_t getTxPower() const override { return _parent->tx_power_dbm; } void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return 0; } - void setAgcResetInt(uint8_t secs) override { /* no-op */ } + uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); } - float getFloodTxDelay() const override { return 0.5f; } // currently hard-coded - void setFloodTxDelay(float d) override { /* no-op */ } - float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded - void setDirectTxDelay(float d) override { /* no-op */ } + float getFloodTxDelay() const override { return _parent->tx_delay_factor; } + void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); } + float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; } + void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); } uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; } void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); } uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; } From 88cc014f55d07470ec906dcdb5163be9695f06d2 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 20:15:45 -0500 Subject: [PATCH 092/174] Add BLE companion support for Pico W --- examples/companion_radio/main.cpp | 4 + src/helpers/rp2040/SerialBLEInterface.cpp | 198 ++++++++++++++++++++++ src/helpers/rp2040/SerialBLEInterface.h | 75 ++++++++ variants/rpi_picow/platformio.ini | 50 ++++-- 4 files changed, 314 insertions(+), 13 deletions(-) create mode 100644 src/helpers/rp2040/SerialBLEInterface.cpp create mode 100644 src/helpers/rp2040/SerialBLEInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ff0794ab..85923a15 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -26,6 +26,10 @@ MultiSerialInterface interface_manager; // include nrf52 bluetooth interface #include SerialBLEInterface bluetooth_interface; + #elif defined(RP2040_PLATFORM) + // include rp2040 (Pico W / CYW43) bluetooth interface + #include + SerialBLEInterface bluetooth_interface; #else #error "SerialBLEInterface is not defined for this platform" #endif diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp new file mode 100644 index 00000000..f0321003 --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -0,0 +1,198 @@ +#include "SerialBLEInterface.h" +#include +#include +#include + +// Nordic UART 6E4000xx-B5A3-F393-E0A9-E50E24DCCA9E, as raw bytes: the core lib's string +// parser uses sscanf("%llx"), which newlib-nano on the RP2040 doesn't support (yields all zeros) +#define NUS_UUID(n) { 0x6E, 0x40, 0x00, n, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E } +static const uint8_t SERVICE_UUID[16] = NUS_UUID(0x01); +static const uint8_t CHARACTERISTIC_UUID_RX[16] = NUS_UUID(0x02); +static const uint8_t CHARACTERISTIC_UUID_TX[16] = NUS_UUID(0x03); + +// The BLE core lib's own setValue()/notify path reallocs the value buffer on every call, +// so a second frame queued before the radio drained the first would corrupt it. We keep our +// own frame queue and drive att_server_notify() from the can-send-now callback instead. + +SerialBLEInterface::SerialBLEInterface() + : BLEService(BLEUUID(SERVICE_UUID)), + _rx(BLEUUID(CHARACTERISTIC_UUID_RX), BLEWrite, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED), + _tx(BLEUUID(CHARACTERISTIC_UUID_TX), BLERead | BLENotify, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED) +{ + _isEnabled = false; + _tx_pending = false; + send_queue_len = 0; + recv_queue_len = 0; + memset(&_can_send, 0, sizeof(_can_send)); + _rx.setCallbacks(this); + addCharacteristic(&_rx); + addCharacteristic(&_tx); +} + +void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { + // JustWorks here only makes the server request pairing on connect; caps are overridden below + BLE.setSecurity(BLESecurityJustWorks); + // adv data carries the 128-bit service UUID, leaving room for only 8 name chars; + // the full name goes in the scan response + BLE.begin(prefix); + + if (strcmp(name, "@@MAC") == 0) { + bd_addr_t a; + gap_local_bd_addr(a); + sprintf(name, "%02X%02X%02X%02X%02X%02X", a[0], a[1], a[2], a[3], a[4], a[5]); // modify (IN-OUT param) + } + char dev_name[32+16]; + snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, name); + + BLE.server()->setName(dev_name); // GAP device name characteristic + BLE.server()->addService(this); + BLE.server()->setCallbacks(this); + + // static passkey pairing with MITM protection, matching the esp32/nrf52 interfaces + sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY); + sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION | SM_AUTHREQ_BONDING); + sm_use_fixed_passkey_in_display_role(pin_code); + + size_t n = strlen(dev_name); + if (n > sizeof(_scan_rsp) - 2) n = sizeof(_scan_rsp) - 2; + _scan_rsp[0] = n + 1; + _scan_rsp[1] = BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME; + memcpy(&_scan_rsp[2], dev_name, n); + gap_scan_response_set_data(n + 2, _scan_rsp); + + BLE_DEBUG_PRINTLN("begin: name=%s", dev_name); +} + +void SerialBLEInterface::clearBuffers() { + send_queue_len = 0; + recv_queue_len = 0; + _tx_pending = false; // a stale registration just fires into an empty queue; BTstack ignores double-adds +} + +void SerialBLEInterface::onConnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("connected handle=0x%04X", _tx.conHandle()); + clearBuffers(); +} + +void SerialBLEInterface::onDisconnect(BLEServer* s) { + BLE_DEBUG_PRINTLN("disconnected"); + clearBuffers(); // BTstack re-enables advertising on its own +} + +void SerialBLEInterface::onWrite(BLECharacteristic* c) { + if (c != &_rx) return; + size_t len = _rx.valueLen(); + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: bad frame len=%u", (unsigned)len); + return; + } + if (recv_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("onWrite: recv queue full, dropping frame"); + return; + } + recv_queue[recv_queue_len].len = len; + memcpy(recv_queue[recv_queue_len].buf, _rx.valueData(), len); + recv_queue_len++; +} + +// caller holds the BT lock (or is in the BT context) +void SerialBLEInterface::kickSend() { + if (_tx_pending) return; + _tx_pending = true; + _can_send.callback = onCanSend; + _can_send.context = this; + if (att_server_register_can_send_now_callback(&_can_send, _tx.conHandle()) != 0) { + _tx_pending = false; + } +} + +// BT context: one notification per can-send-now, re-arm while frames remain +void SerialBLEInterface::sendNext() { + _tx_pending = false; + if (send_queue_len == 0) return; + if (!isConnected()) { + BLE_DEBUG_PRINTLN("sendNext: not connected, clearing send queue"); + send_queue_len = 0; + return; + } + uint16_t h = _tx.conHandle(); + Frame& f = send_queue[0]; + uint16_t mtu = att_server_get_mtu(h); + if (f.len + 3 > mtu) { + // att would silently truncate; drop instead (client must negotiate MTU >= MAX_FRAME_SIZE+3) + BLE_DEBUG_PRINTLN("sendNext: frame len=%u exceeds mtu=%u, dropping", f.len, mtu); + } else { + uint8_t err = att_server_notify(h, _tx.valueHandle(), f.buf, f.len); + if (err == BTSTACK_ACL_BUFFERS_FULL) { + kickSend(); + return; + } + if (err) { + BLE_DEBUG_PRINTLN("sendNext: notify failed err=%u, dropping", err); + } else { + BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", f.len, f.buf[0]); + } + } + send_queue_len--; + memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame)); + if (send_queue_len > 0) kickSend(); +} + +void SerialBLEInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); + BLE.startAdvertising(true); +} + +void SerialBLEInterface::disconnect() { + uint16_t h = _tx.conHandle(); + if (h) gap_disconnect(h); +} + +void SerialBLEInterface::disable() { + _isEnabled = false; + BLE_DEBUG_PRINTLN("disable"); + disconnect(); + BLE.stopAdvertising(); +} + +bool SerialBLEInterface::isConnected() const { + // notifications can only be enabled once the link is authenticated (CCCD inherits the perms) + return _isEnabled && _tx.conHandle() != 0 && _tx.notifyEnabled(); +} + +bool SerialBLEInterface::isWriteBusy() const { + return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); +} + +size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { + if (len == 0 || len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len); + return 0; + } + if (!isConnected()) return 0; + + BluetoothLock lock; + if (send_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + send_queue[send_queue_len].len = len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + kickSend(); + return len; +} + +size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { + BluetoothLock lock; + if (recv_queue_len == 0) return 0; + + size_t len = recv_queue[0].len; + memcpy(dest, recv_queue[0].buf, len); + recv_queue_len--; + memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame)); + BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]); + return len; +} diff --git a/src/helpers/rp2040/SerialBLEInterface.h b/src/helpers/rp2040/SerialBLEInterface.h new file mode 100644 index 00000000..2e085a6c --- /dev/null +++ b/src/helpers/rp2040/SerialBLEInterface.h @@ -0,0 +1,75 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include +#include + +// Nordic UART service over the arduino-pico BLE library (BTstack on the CYW43). +// Build with -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH so the core links liblwip-bt. +class SerialBLEInterface : public BaseSerialInterface, BLEService, BLEServerCallbacks, BLECharacteristicCallbacks { + // subclass only to reach the protected connection/notify state + struct Characteristic : public BLECharacteristic { + using BLECharacteristic::BLECharacteristic; + uint16_t valueHandle() const { return _valueHandle; } + uint16_t conHandle() const { return con_handle; } + bool notifyEnabled() const { return _notificationEnabled; } + }; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 8 + + Characteristic _rx; + Characteristic _tx; + bool _isEnabled; + bool _tx_pending; // a can-send-now callback is registered + btstack_context_callback_registration_t _can_send; + uint8_t _scan_rsp[31]; // complete local name; BTstack keeps the pointer + + uint8_t send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + uint8_t recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers(); + void kickSend(); + void sendNext(); + static void onCanSend(void* ctx) { ((SerialBLEInterface*)ctx)->sendNext(); } + + // BLE library callbacks (run in the BT context) + void onWrite(BLECharacteristic* c) override; + void onConnect(BLEServer* s) override; + void onDisconnect(BLEServer* s) override; + +public: + SerialBLEInterface(); + + /** + * init the BLE interface. + * @param prefix a prefix for the device name + * @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned + * @param pin_code the BLE security pin + */ + void begin(const char* prefix, char* name, uint32_t pin_code); + + void disconnect(); + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + bool isConnected() const override; + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if BLE_DEBUG_LOGGING && ARDUINO + #include + #define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__) + #define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__) +#else + #define BLE_DEBUG_PRINT(...) {} + #define BLE_DEBUG_PRINTLN(...) {} +#endif diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 32944a9a..c4180650 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -67,19 +67,43 @@ lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 lib_ignore = BLE -; [env:PicoW_companion_radio_ble] -; extends = rpi_picow -; build_flags = ${rpi_picow.build_flags} -; -D MAX_CONTACTS=100 -; -D MAX_GROUP_CHANNELS=8 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 -; ; -D MESH_PACKET_LOGGING=1 -; ; -D MESH_DEBUG=1 -; build_src_filter = ${rpi_picow.build_src_filter} -; +<../examples/companion_radio/*.cpp> -; lib_deps = ${rpi_picow.lib_deps} -; densaugeo/base64 @ ~1.4.0 +[env:PicoW_companion_radio_ble] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; USB + WiFi + BLE together; the interface manager fans frames out to all of them +[env:PicoW_companion_radio_all] +extends = rpi_picow +build_flags = ${rpi_picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE + -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rpi_picow.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${rpi_picow.lib_deps} + densaugeo/base64 @ ~1.4.0 [env:PicoW_companion_radio_wifi] extends = rpi_picow From b9fa450f52512486ce4b19bd09b71c383cfb2e29 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 20:43:37 -0500 Subject: [PATCH 093/174] Fix review findings on Pico W WiFi/BLE --- examples/companion_radio/MyMesh.cpp | 3 ++- examples/companion_radio/main.cpp | 21 +++++++++++++-------- src/helpers/rp2040/SerialBLEInterface.cpp | 3 +++ variants/rpi_picow/platformio.ini | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a8e305a6..4eea0b2f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2158,7 +2158,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } #ifdef WIFI_SSID - // local console only: these are credentials, and remote admin has no business with them + // not accepted from the remote mesh CLI (timestamp != 0): these are credentials. The app + // over USB/BLE/WiFi and the serial console (timestamp 0) may set them. if (sender_timestamp == 0) { if (memcmp(command, "set wifi.ssid ", 14) == 0) { StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 85923a15..6b6d7cf7 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -41,7 +41,11 @@ MultiSerialInterface interface_manager; #define TCP_PORT 5000 #endif #ifndef WIFI_RETRY_INTERVAL - #define WIFI_RETRY_INTERVAL 30000 // millis between reconnect attempts + #if defined(RP2040_PLATFORM) + #define WIFI_RETRY_INTERVAL 30000 // each attempt blocks loop(), so retry less often + #else + #define WIFI_RETRY_INTERVAL 10000 // millis between reconnect attempts + #endif #endif #ifndef WIFI_RETRY_TIMEOUT #define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop() @@ -120,8 +124,8 @@ void halt() { #ifdef WIFI_SSID bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; - const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set - const char* wifi_pwd = WIFI_PWD; + char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set + char wifi_pwd[64] = WIFI_PWD; bool wifi_was_connected = false; #endif @@ -220,13 +224,14 @@ void setup() { #endif // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). - // they are taken as a pair, so 'set wifi.ssid' alone gives an open-network join, not a - // silent fallback to the build-time password of a different network. + // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a + // silent fallback to the build-time password of a different network. Copied out of prefs + // so 'set wifi.*' edits only take effect on reboot, as their replies promise. + // (No NULL-for-open-network: the RP2040 core does strlen() on the password unguarded.) if (the_mesh.getNodePrefs()->wifi_ssid[0]) { - wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid; - wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd; + strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); + strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - if (wifi_pwd[0] == 0) wifi_pwd = NULL; // NULL (not "") selects an open network WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp index f0321003..a1fa487e 100644 --- a/src/helpers/rp2040/SerialBLEInterface.cpp +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -1,3 +1,5 @@ +// only built when the env enables the core BLE stack (build_as_lib.py globs this dir) +#ifdef PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH #include "SerialBLEInterface.h" #include #include @@ -196,3 +198,4 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]); return len; } +#endif diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index c4180650..f99b1341 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -92,8 +92,8 @@ build_flags = ${rpi_picow.build_flags} -D ENABLE_USB_INTERFACE -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 - -D WIFI_DEBUG_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) +; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' ; -D MESH_PACKET_LOGGING=1 From b11a779843fc949ec7589de4fc7fba2f84750d25 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:28:04 -0500 Subject: [PATCH 094/174] Address review: per-variant wifi src, real SSID reply, rename PicoW env --- examples/companion_radio/MyMesh.cpp | 2 +- platformio.ini | 1 - variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/lilygo_tbeam_1w/platformio.ini | 1 + variants/lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/meshnology_w12/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 2 +- variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 24 files changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4eea0b2f..e835d8ef 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2181,7 +2181,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "(build-time)"); + sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } } diff --git a/platformio.ini b/platformio.ini index 622b01e2..2219c978 100644 --- a/platformio.ini +++ b/platformio.ini @@ -64,7 +64,6 @@ build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} - + [esp32_ota] lib_deps = diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 354004f0..df986cf0 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -185,6 +185,7 @@ build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -324,6 +325,7 @@ build_flags = build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d040b72f..178508d8 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -195,6 +195,7 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 78561a14..28e80554 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -189,6 +189,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 65e636eb..27541a8a 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -198,6 +198,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -350,6 +351,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index d718c006..25f9ee3b 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -241,6 +241,7 @@ build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -406,6 +407,7 @@ build_src_filter = ${heltec_v4_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index f523ebf9..8c2feb94 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -186,6 +186,7 @@ build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -311,6 +312,7 @@ build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 0f604fac..16db0257 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -165,6 +165,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 8bfc4093..a9991f25 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -160,6 +160,7 @@ build_flags = ; -D CORE_DEBUG_LEVEL=4 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 1aea74d2..cf24a5d5 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=128 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 8255e46c..c0e8c80d 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -184,6 +184,7 @@ build_src_filter = ${meshnology_w12.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 112181df..3c5049e0 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -154,6 +154,7 @@ build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 1161743e..9789eabf 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -150,6 +150,7 @@ build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 8bd3c597..2fee8c26 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -182,6 +182,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index f99b1341..e54d86a6 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -84,7 +84,7 @@ lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 ; USB + WiFi + BLE together; the interface manager fans frames out to all of them -[env:PicoW_companion_radio_all] +[env:PicoW_companion_radio] extends = rpi_picow build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index bdb7ee0c..d8bdd5e8 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -234,6 +234,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 074d6a2e..477c135b 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -149,6 +149,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index 583f913c..c1f5612a 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -184,6 +184,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M2.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 5e85b649..f64e4e77 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -198,6 +198,7 @@ build_flags = -D WIFI_PWD='"mypwd"' build_src_filter = ${ThinkNode_M5.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 7d9892e5..508fc813 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -145,6 +145,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 09b1391d..085ab7d5 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -146,6 +146,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M9.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c9c107c6..cca2907a 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -115,6 +115,7 @@ extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} +<../examples/companion_radio/*.cpp> + + + build_flags = ${Xiao_esp32_C3.build_flags} -D MAX_CONTACTS=350 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 293a13c1..de656c44 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -202,6 +202,7 @@ extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> build_flags = From 98cf4f3332b8f537e36979c1ccff1f6e12766048 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:44:47 -0500 Subject: [PATCH 095/174] Add get wifi.status and get wifi.ip CLI commands --- examples/companion_radio/MyMesh.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e835d8ef..76e0dfde 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,6 +1,9 @@ #include "MyMesh.h" #include // needed for PlatformIO +#ifdef WIFI_SSID +#include +#endif #include #define CMD_APP_START 1 @@ -2184,6 +2187,18 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } + if (strcmp(command, "get wifi.status") == 0) { + strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); + } else { + strcpy(reply, "> (not connected)"); + } + return true; + } } #endif From eb2ab10de0a23ab76e79379ee09109260c9f4efa Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 21:56:25 -0500 Subject: [PATCH 096/174] Add wifi.enabled pref and CLI; skip WiFi when disabled or SSID blank --- examples/companion_radio/MyMesh.cpp | 10 +++ examples/companion_radio/NodePrefs.h | 2 + examples/companion_radio/main.cpp | 95 +++++++++++++++------------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 76e0dfde..2886ceb6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2187,6 +2187,16 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); return true; } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + savePrefs(); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + sprintf(reply, "> %d", _prefs.wifi_enabled); + return true; + } if (strcmp(command, "get wifi.status") == 0) { strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); return true; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index c725c317..03323675 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,6 +47,7 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; + uint8_t wifi_enabled = 1; // 0 = never bring up WiFi (credentials may still be baked in) #endif private: @@ -171,6 +172,7 @@ private: void structure() override { def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid)); def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd)); + def("enabled", _parent->wifi_enabled); } public: WiFiPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6b6d7cf7..cf761095 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -127,6 +127,7 @@ void halt() { char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set char wifi_pwd[64] = WIFI_PWD; bool wifi_was_connected = false; + bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank #endif void setup() { @@ -208,21 +209,6 @@ void setup() { // add wifi interface #ifdef WIFI_SSID -#if defined(ESP32) - board.setInhibitSleep(true); // prevent sleep when WiFi is active - WiFi.setAutoReconnect(true); - - WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ - if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); - wifi_needs_reconnect = true; - } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { - WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); - wifi_needs_reconnect = false; - } - }); -#endif - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a // silent fallback to the build-time password of a different network. Copied out of prefs @@ -232,20 +218,41 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + // 'set wifi.enabled 0' or a build with blank credentials leaves the radio off entirely + wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled && wifi_ssid[0]; + if (wifi_enabled) { +#if defined(ESP32) + board.setInhibitSleep(true); // prevent sleep when WiFi is active + WiFi.setAutoReconnect(true); + + WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + wifi_needs_reconnect = true; + } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { + WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + wifi_needs_reconnect = false; + } + }); +#endif + + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the - // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the - // extra DHCP wait. Give the first connect a full window, then bound the retries below. - // Upgrade path if the stall ever matters: run WiFi on core1. - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); - last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry + // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the + // extra DHCP wait. Give the first connect a full window, then bound the retries below. + // Upgrade path if the stall ever matters: run WiFi on core1. + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); + last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else - WiFi.begin(wifi_ssid, wifi_pwd); + WiFi.begin(wifi_ssid, wifi_pwd); #endif - wifi_interface.begin(TCP_PORT); - interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + } else { + WIFI_DEBUG_PRINTLN("wifi disabled"); + } #endif // add usb interface @@ -301,31 +308,33 @@ void loop() { } #ifdef WIFI_SSID - // RP2040 has no WiFi event callbacks, so poll the link state instead + if (wifi_enabled) { + // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) - wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); - if (wifi_was_connected == wifi_needs_reconnect) { // link state changed - wifi_was_connected = !wifi_needs_reconnect; - if (wifi_was_connected) { - WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); - } else { - WIFI_DEBUG_PRINTLN("link lost"); + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + wifi_was_connected = !wifi_needs_reconnect; + if (wifi_was_connected) { + WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); + } else { + WIFI_DEBUG_PRINTLN("link lost"); + } } - } #endif - // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop - // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); + // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop + // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); #if defined(RP2040_PLATFORM) - WiFi.setTimeout(WIFI_RETRY_TIMEOUT); - WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform #else - WiFi.disconnect(); - WiFi.reconnect(); + WiFi.disconnect(); + WiFi.reconnect(); #endif - last_wifi_reconnect_attempt = millis(); + last_wifi_reconnect_attempt = millis(); + } } #endif } From dac171b49c23ab11a9906a80f55ea0e5d14c9b0f Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 22:05:29 -0500 Subject: [PATCH 097/174] Default wifi.enabled to 0 --- examples/companion_radio/NodePrefs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 03323675..68facfbe 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,7 +47,7 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 1; // 0 = never bring up WiFi (credentials may still be baked in) + uint8_t wifi_enabled = 0; // off until 'set wifi.enabled 1' (credentials may still be baked in) #endif private: From 7c2b4d88be891d884907875172dfc99beb1c48b0 Mon Sep 17 00:00:00 2001 From: Michael Graff Date: Sun, 6 Sep 2026 22:18:00 -0500 Subject: [PATCH 098/174] WiFi on/off tri-state: on when SSID set unless explicitly disabled; PicoW ships no default SSID --- examples/companion_radio/MyMesh.cpp | 13 +++++++++---- examples/companion_radio/NodePrefs.h | 7 ++++++- examples/companion_radio/main.cpp | 4 ++-- variants/rpi_picow/platformio.ini | 8 ++++---- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2886ceb6..b2c99288 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2184,17 +2184,22 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : WIFI_SSID); + sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { - _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + uint8_t en = atoi(&command[17]) ? 1 : 0; + if (en && !_prefs.wifiSSID()[0]) { + strcpy(reply, "> set wifi.ssid first"); + return true; + } + _prefs.wifi_enabled = en; savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", en); return true; } if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifi_enabled); + sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); return true; } if (strcmp(command, "get wifi.status") == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 68facfbe..85cdebb2 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,7 +47,12 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 0; // off until 'set wifi.enabled 1' (credentials may still be baked in) + uint8_t wifi_enabled = 2; // 0 = off, 1 = on, 2 = never set (treated as on) + + // effective SSID: stored prefs win over the build-time one + const char* wifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } + // WiFi runs only when there is an SSID and it hasn't been explicitly turned off + bool wifiEnabled() const { return wifiSSID()[0] && wifi_enabled != 0; } #endif private: diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index cf761095..d461a36d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -218,8 +218,8 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - // 'set wifi.enabled 0' or a build with blank credentials leaves the radio off entirely - wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled && wifi_ssid[0]; + // 'set wifi.enabled 0', or no SSID from either prefs or the build, leaves the radio off entirely + wifi_enabled = the_mesh.getNodePrefs()->wifiEnabled(); if (wifi_enabled) { #if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index e54d86a6..763c96bc 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -94,8 +94,8 @@ build_flags = ${rpi_picow.build_flags} -D BLE_PIN_CODE=123456 ; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) ; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='"myssid"' - -D WIFI_PWD='"mypwd"' + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} @@ -111,8 +111,8 @@ build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='"myssid"' - -D WIFI_PWD='"mypwd"' + -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' + -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} From d71a54cd3dfe8dc5eca7c0e0834c6e6e3ea18837 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 7 Sep 2026 13:56:47 +1000 Subject: [PATCH 099/174] * CLI: removed "sub" and "unsub" (cannot support via command line) * _SUBSCRIBE: binary req/resp payload changes (now keeps supplied 'push_tag') * Sensor: added MCU temperature support in telemetry --- examples/simple_sensor/SensorMesh.cpp | 112 ++++++-------------------- src/helpers/ClientACL.h | 1 + 2 files changed, 27 insertions(+), 86 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index c3f2d9b0..8c6b06a6 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -55,8 +55,10 @@ #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define REQ_TYPE_GET_ACCESS_LIST 0x05 -#define REQ_TYPE_SUBSCRIBE 0x10 -#define REQ_TYPE_UNSUBSCRIBE 0x11 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present) + +#define REQ_TYPE_SUBSCRIBE 0x08 +#define REQ_TYPE_UNSUBSCRIBE 0x09 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -193,43 +195,6 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u return 0.0f; // not found } -static uint8_t compileLPPSpec(char* txt, uint8_t* dest, size_t max_len) { - const char* parts[3]; - int n = mesh::Utils::parseTextParts(txt, parts, 3, ','); - uint8_t len = 0; - for (int i = 0; i < n && len + 6 <= max_len; i++) { - const char* cp = strchr(parts[i], ':'); - if (cp) { - uint8_t t; - float factor = 1.0f; - cp++; // skip the ':' - char* ep = strchr(cp, 0) - 1; // find LAST char - if (*ep == 'V') { // Volts - t = LPP_VOLTAGE; - } else if (*ep == 'W') { // Watts - t = LPP_POWER; - } else if (*ep == 'C') { // Celcius - t = LPP_TEMPERATURE; - } else if (*ep == 'P') { // Pascals - t = LPP_BAROMETRIC_PRESSURE; - } else if (*ep == 'A') { // Amps - t = LPP_CURRENT; - } else if (*ep == 'm') { - t = LPP_DISTANCE; factor = 0.001f; - } else { - t = 0; - } - - if (t) { - dest[len++] = atoi(parts[i]); // channel number - dest[len++] = t; // LPP type - len += putFloat(&dest[len], atof(cp) * factor, getDataSize(t), getMultiplier(t), isSigned(t)); - } - } - } - return len; -} - /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { @@ -269,6 +234,10 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u // query other sensors -- target specific sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); @@ -322,10 +291,11 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u return ofs; } } - if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 4 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { + memcpy(&from->extra.sensor.push_tag, &payload[0], 4); uint16_t timeout_secs; - memcpy(&timeout_secs, &payload[0], 2); - uint8_t reserved = payload[2]; + memcpy(&timeout_secs, &payload[4], 2); + uint8_t reserved = payload[6]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; @@ -334,16 +304,17 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u } from->extra.sensor.scope_region_id = r ? r->id : 0; from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = payload[3]; + from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[4], min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[3])); + memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 6 + strlen((char *)&reply_data[8]); + return 8 + strlen((char *)&reply_data[8]); } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; + from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? reply_data[4] = 0; // success @@ -597,43 +568,6 @@ void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char Serial.printf("\n"); } reply[0] = 0; - } else if (from != NULL && memcmp(command, "sub", 3) == 0 && (command[3] == ' ' || command[3] == 0)) { // subscribe - uint8_t perms = from->isAdmin() ? 0xFF : from->permissions; - if ((perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { - RegionEntry* r; - if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope - r = recv_pkt_region; - } else { // use default scope - r = region_map.getDefaultRegion(); - } - // defaults: - from->extra.sensor.min_deltas_len = 0; // no minimums (telemetry just needs to CHANGE) - uint16_t timeout_secs = 30*60; // expires after 30 mins - if (command[3] == ' ') { // eg. "sub 300 1:0.2V" - char* cp = &command[4]; - while (*cp >= '0' && *cp <= '9') cp++; - if (cp > &command[4]) { - timeout_secs = atoi(&command[4]); - if (*cp == ' ') { - cp++; // skip the space - from->extra.sensor.min_deltas_len = compileLPPSpec(cp, from->extra.sensor.min_deltas, sizeof(from->extra.sensor.min_deltas)); - } - } - } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - if (from->extra.sensor.expiry_timestamp) { - DateTime dt = DateTime(from->extra.sensor.expiry_timestamp); - sprintf(reply, "OK - sub expires: %02d:%02d (UTC)", dt.hour(), dt.minute()); - } else { - strcpy(reply, "Err - region scope needed"); - } - } else { - strcpy(reply, "Err - no permission"); - } - } else if (from != NULL && strcmp(command, "unsub") == 0) { // unsubscribe - from->extra.sensor.scope_region_id = 0; - strcpy(reply, "OK - unsubscribed"); } else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read if (command[2] == ' ') { // it's a write uint32_t val; @@ -1172,6 +1106,11 @@ void SensorMesh::loop() { telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + // This MCU temperature will be overridden by external sensors (if any) + float temperature = board.getMCUTemperature(); + if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { @@ -1184,11 +1123,12 @@ void SensorMesh::loop() { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); // this will be an unknown 'tag' to the client - memcpy(reply_data, ×tamp, 4); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + memcpy(reply_data, &c->extra.sensor.push_tag, 4); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(&reply_data[4], ×tamp, 4); + memcpy(&reply_data[8], telemetry.getBuffer(), tlen); - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 4 + tlen); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen); if (reply) { if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, c->out_path, c->out_path_len, 0); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 1e9f472d..39e71f2c 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -30,6 +30,7 @@ struct ClientInfo { } room; struct { uint32_t expiry_timestamp; // epoch seconds + uint32_t push_tag; uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded From 35142a8896733f6815b6a43bea2367d570059e7c Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 2 Mar 2026 10:11:51 +0100 Subject: [PATCH 100/174] Fix 1970 date after crash/watchdog/brownout reset on ESP32 Currently, time is only set to ~2024 when it powers on cleanly. When time is lost, this results in time dropping to 1970. Save time as backup to RTC slow memory. Should fix time staying accurate during brownouts etc. --- src/helpers/ESP32Board.h | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 75428bc6..3d1b97f4 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -200,19 +200,30 @@ public: } }; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_POWERON) { - // start with some date/time in the recent past - struct timeval tv; - tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm + if (reason == ESP_RST_DEEPSLEEP) { + return; // ESP-IDF preserves system time across deep sleep + } + // All other resets (power-on, crash, WDT, brownout) lose system time. + // Restore from RTC backup if valid, otherwise use hardcoded seed. + struct timeval tv; + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + tv.tv_sec = _rtc_backup_time; + } else { + tv.tv_sec = 1772323200; // 1 Mar 2026 + } tv.tv_usec = 0; settimeofday(&tv, NULL); } - } uint32_t getCurrentTime() override { time_t _now; time(&_now); @@ -223,6 +234,16 @@ public: tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + void tick() override { + time_t now; + time(&now); + if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { + _rtc_backup_time = (uint32_t)now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } } }; From cb6f2f22555826d1392262b1a2c24661b988c3c8 Mon Sep 17 00:00:00 2001 From: Wessel Date: Fri, 5 Jun 2026 19:00:33 +0200 Subject: [PATCH 101/174] Update src/helpers/ESP32Board.h Co-authored-by: Dan McGee --- src/helpers/ESP32Board.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 3d1b97f4..3f95e692 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -219,7 +219,7 @@ public: if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { tv.tv_sec = _rtc_backup_time; } else { - tv.tv_sec = 1772323200; // 1 Mar 2026 + tv.tv_sec = RTC_TIME_MIN; } tv.tv_usec = 0; settimeofday(&tv, NULL); From 790680e4d13eb847421d6efc0fe283e0496aaaf5 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:25:34 +1200 Subject: [PATCH 102/174] allow toggling wifi enabled regardless of stored ssid --- examples/companion_radio/MyMesh.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b2c99288..9a5986f9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2188,14 +2188,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { - uint8_t en = atoi(&command[17]) ? 1 : 0; - if (en && !_prefs.wifiSSID()[0]) { - strcpy(reply, "> set wifi.ssid first"); - return true; - } - _prefs.wifi_enabled = en; + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", en); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); return true; } if (strcmp(command, "get wifi.enabled") == 0) { From bf64fafe9006b1b709c417753773f8ba96cbf714 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:30:00 +1200 Subject: [PATCH 103/174] simplify response message --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9a5986f9..f745ab2b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2180,7 +2180,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; savePrefs(); - strcpy(reply, "> wifi config cleared, using build-time credentials (reboot to apply)"); + strcpy(reply, "> wifi config cleared (reboot to apply)"); return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design From 87a05e15896143972690aa8b1bf828fb76c088af Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 18:33:37 +1200 Subject: [PATCH 104/174] allow remote management of wifi as user has cli permission --- examples/companion_radio/MyMesh.cpp | 90 ++++++++++++++--------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f745ab2b..4e85e3cb 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2161,54 +2161,50 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } #ifdef WIFI_SSID - // not accepted from the remote mesh CLI (timestamp != 0): these are credentials. The app - // over USB/BLE/WiFi and the serial console (timestamp 0) may set them. - if (sender_timestamp == 0) { - if (memcmp(command, "set wifi.ssid ", 14) == 0) { - StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); - savePrefs(); - sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); - return true; - } - if (memcmp(command, "set wifi.pwd ", 13) == 0) { - StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); - savePrefs(); - strcpy(reply, "> wifi.pwd updated (reboot to apply)"); - return true; - } - if (strcmp(command, "set wifi.clear") == 0) { - _prefs.wifi_ssid[0] = 0; - _prefs.wifi_pwd[0] = 0; - savePrefs(); - strcpy(reply, "> wifi config cleared (reboot to apply)"); - return true; - } - if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); - return true; - } - if (memcmp(command, "set wifi.enabled ", 17) == 0) { - _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; - savePrefs(); - sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); - return true; - } - if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); - return true; - } - if (strcmp(command, "get wifi.status") == 0) { - strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); - return true; - } - if (strcmp(command, "get wifi.ip") == 0) { - if (WiFi.status() == WL_CONNECTED) { - sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); - } else { - strcpy(reply, "> (not connected)"); - } - return true; + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); + savePrefs(); + sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); + savePrefs(); + strcpy(reply, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "set wifi.clear") == 0) { + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + savePrefs(); + strcpy(reply, "> wifi config cleared (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); + return true; + } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + savePrefs(); + sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { + sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); + return true; + } + if (strcmp(command, "get wifi.status") == 0) { + strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + sprintf(reply, "> %s", WiFi.localIP().toString().c_str()); + } else { + strcpy(reply, "> (not connected)"); } + return true; } #endif From 974f00ded051663b738584fc5c2523dd240b54f3 Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 8 Sep 2026 16:35:31 +1000 Subject: [PATCH 105/174] fix: LR2021 to use correct macro for IRQ_DETECTED in startReceive() --- src/helpers/radiolib/CustomLR2021.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index a89ae943..0255b93c 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -72,7 +72,7 @@ class CustomLR2021 : public LR2021 { int16_t startReceive() override { // include the PREAMBLE_DETECTED irq bit in reported flags - return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } bool isReceiving() { From 8660d1c973d1770275fe36cd990623a5a807b204 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 8 Sep 2026 17:55:02 +1000 Subject: [PATCH 106/174] * refactor: LPP helpers moved to LPPData class --- examples/simple_sensor/SensorMesh.cpp | 126 +++----------------- src/helpers/sensors/LPPDataHelpers.h | 159 ++++++++++++++++++-------- 2 files changed, 128 insertions(+), 157 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 8c6b06a6..de58e2b1 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,5 +1,6 @@ #include "SensorMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -80,103 +81,6 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -static uint8_t getDataSize(uint8_t type) { - switch (type) { - case LPP_GPS: - return 9; - case LPP_POLYLINE: - return 8; // TODO: this is MINIMIUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - return 6; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - return 4; - case LPP_COLOUR: - return 3; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - return 2; - } - return 1; -} - -static uint32_t getMultiplier(uint8_t type) { - switch (type) { - case LPP_CURRENT: - case LPP_DISTANCE: - case LPP_ENERGY: - return 1000; - case LPP_VOLTAGE: - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - return 100; - case LPP_TEMPERATURE: - case LPP_BAROMETRIC_PRESSURE: - case LPP_RELATIVE_HUMIDITY: - return 10; - } - return 1; -} - -static bool isSigned(uint8_t type) { - return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || - type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; -} - -static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { - uint32_t value = 0; - for (uint8_t i = 0; i < size; i++) { - value = (value << 8) + buffer[i]; - } - - int sign = 1; - if (is_signed) { - uint32_t bit = 1ul << ((size * 8) - 1); - if ((value & bit) == bit) { - value = (bit << 1) - value; - sign = -1; - } - } - return sign * ((float) value / multiplier); -} - -static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { - // check sign - bool sign = value < 0; - if (sign) value = -value; - - // get value to store - uint32_t v = value * multiplier; - - // format an uint32_t as if it was an int32_t - if (is_signed & sign) { - uint32_t mask = (1 << (size * 8)) - 1; - v = v & mask; - if (sign) v = mask - v + 1; - } - - // add bytes (MSB first) - for (uint8_t i=1; i<=size; i++) { - dest[size - i] = (v & 0xFF); - v >>= 8; - } - return size; -} - static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { uint8_t i = 0; @@ -185,10 +89,10 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); if (ch == channel && t == type) { - return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); } i += sz; // skip } @@ -209,10 +113,10 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l uint8_t ch = buf[i++]; // Get data type uint8_t t = buf[i++]; - uint8_t sz = getDataSize(t); + uint8_t sz = LPPData::getDataSize(t); - float v = getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); - float pv = getFloat(&prev_telem[i], sz, getMultiplier(t), isSigned(t)); + float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); if (abs(v - pv) > min_delta) return true; // Yes, has changed @@ -268,12 +172,12 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u auto d = &data[i]; reply_data[ofs++] = d->_channel; reply_data[ofs++] = d->_lpp_type; - uint8_t sz = getDataSize(d->_lpp_type); - uint32_t mult = getMultiplier(d->_lpp_type); - bool is_signed = isSigned(d->_lpp_type); - ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); - ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); + uint8_t sz = LPPData::getDataSize(d->_lpp_type); + uint32_t mult = LPPData::getMultiplier(d->_lpp_type); + bool is_signed = LPPData::isSigned(d->_lpp_type); + ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); } return ofs; } @@ -317,9 +221,9 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? - reply_data[4] = 0; // success - getRNG()->random(&reply_data[5], 3); // just some entropy for better packet-hash uniqueness - return 8; + memset(&reply_data[4], 0, 8); // success + getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness + return 12 + 2; } return 0; // unknown command } diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 70a036c4..db49cade 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -63,12 +63,66 @@ #define LPP_ERROR_OVERFLOW 1 #define LPP_ERROR_UNKOWN_TYPE 2 -class LPPReader { - const uint8_t* _buf; - uint8_t _len; - uint8_t _pos; +class LPPData { +public: + static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; + } - float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + return 10; + } + return 1; + } + + static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; + } + + static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { uint32_t value = 0; for (uint8_t i = 0; i < size; i++) { value = (value << 8) + buffer[i]; @@ -85,6 +139,36 @@ class LPPReader { return sign * ((float) value / multiplier); } + static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { + // check sign + bool sign = value < 0; + if (sign) value = -value; + + // get value to store + uint32_t v = value * multiplier; + + // format an uint32_t as if it was an int32_t + if (is_signed & sign) { + uint32_t mask = (1 << (size * 8)) - 1; + v = v & mask; + if (sign) v = mask - v + 1; + } + + // add bytes (MSB first) + for (uint8_t i=1; i<=size; i++) { + dest[size - i] = (v & 0xFF); + v >>= 8; + } + return size; + } + +}; + +class LPPReader { + const uint8_t* _buf; + uint8_t _len; + uint8_t _pos; + public: LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { } @@ -103,72 +187,42 @@ public: } bool readGPS(float& lat, float& lon, float& alt) { - lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; - alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3; + lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3; + alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3; return _pos <= _len; } bool readVoltage(float& voltage) { - voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2; + voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2; return _pos <= _len; } bool readCurrent(float& amps) { - amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; + amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2; return _pos <= _len; } bool readPower(float& watts) { - watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2; + watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2; return _pos <= _len; } bool readTemperature(float& degrees_c) { - degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2; + degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2; return _pos <= _len; } bool readPressure(float& pa) { - pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2; + pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2; return _pos <= _len; } bool readRelativeHumidity(float& pct) { - pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1; + pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1; return _pos <= _len; } bool readAltitude(float& m) { - m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2; + m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2; return _pos <= _len; } void skipData(uint8_t type) { - switch (type) { - case LPP_GPS: - _pos += 9; break; - case LPP_POLYLINE: - _pos += 8; break; // TODO: this is MINIMUM - case LPP_GYROMETER: - case LPP_ACCELEROMETER: - _pos += 6; break; - case LPP_GENERIC_SENSOR: - case LPP_FREQUENCY: - case LPP_DISTANCE: - case LPP_ENERGY: - case LPP_UNIXTIME: - _pos += 4; break; - case LPP_COLOUR: - _pos += 3; break; - case LPP_ANALOG_INPUT: - case LPP_ANALOG_OUTPUT: - case LPP_LUMINOSITY: - case LPP_TEMPERATURE: - case LPP_CONCENTRATION: - case LPP_BAROMETRIC_PRESSURE: - case LPP_ALTITUDE: - case LPP_VOLTAGE: - case LPP_CURRENT: - case LPP_DIRECTION: - case LPP_POWER: - _pos += 2; break; - default: - _pos++; - } + _pos += LPPData::getDataSize(type); } }; @@ -185,6 +239,19 @@ class LPPWriter { public: LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { } + bool writeData(uint8_t channel, uint8_t type, float v) { + uint8_t sz = LPPData::getDataSize(type); + bool s = LPPData::isSigned(type); + uint32_t mul = LPPData::getMultiplier(type); + if (_len + 2 + sz <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = type; + _len += LPPData::putFloat(&_buf[_len], v, sz, mul, s); + return true; + } + return false; + } + bool writeVoltage(uint8_t channel, float voltage) { if (_len + 4 <= _max_len) { _buf[_len++] = channel; From 6809fed159444e6f8094b5abc3811ab46db4fdff Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 21:30:17 +1200 Subject: [PATCH 107/174] simplify and remove 2 as a wifi enabled state --- examples/companion_radio/MyMesh.cpp | 4 ++-- examples/companion_radio/NodePrefs.h | 9 +++------ examples/companion_radio/main.cpp | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4e85e3cb..7a380e98 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2181,7 +2181,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design - sprintf(reply, "> %s", _prefs.wifiSSID()[0] ? _prefs.wifiSSID() : "(not set)"); + sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); return true; } if (memcmp(command, "set wifi.enabled ", 17) == 0) { @@ -2191,7 +2191,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } if (strcmp(command, "get wifi.enabled") == 0) { - sprintf(reply, "> %d", _prefs.wifiEnabled() ? 1 : 0); + sprintf(reply, "> %d", _prefs.wifi_enabled); return true; } if (strcmp(command, "get wifi.status") == 0) { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 85cdebb2..ed408f9a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -47,12 +47,9 @@ public: #ifdef WIFI_SSID char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; - uint8_t wifi_enabled = 2; // 0 = off, 1 = on, 2 = never set (treated as on) - - // effective SSID: stored prefs win over the build-time one - const char* wifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } - // WiFi runs only when there is an SSID and it hasn't been explicitly turned off - bool wifiEnabled() const { return wifiSSID()[0] && wifi_enabled != 0; } + uint8_t wifi_enabled = 1; // enabled by default to allow wifi only builds to work. wifi won't be started if ssid is empty + // use ssid from prefs, or fallback to ssid from build flags + const char* getWifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } #endif private: diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d461a36d..bfcd2682 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -218,9 +218,9 @@ void setup() { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); } - // 'set wifi.enabled 0', or no SSID from either prefs or the build, leaves the radio off entirely - wifi_enabled = the_mesh.getNodePrefs()->wifiEnabled(); - if (wifi_enabled) { + // only start wifi if enabled and ssid is not empty + wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled; + if (wifi_enabled && wifi_ssid[0]) { #if defined(ESP32) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); From cced091b0c5715d9477d2cde3758e679ad8d4455 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 22:18:22 +1200 Subject: [PATCH 108/174] tidy comments --- examples/companion_radio/main.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index bfcd2682..d886b804 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -209,11 +209,7 @@ void setup() { // add wifi interface #ifdef WIFI_SSID - // stored credentials win over the build-time ones ('set wifi.ssid ' over USB serial). - // they are taken as a pair, so 'set wifi.ssid' alone gives an empty password, not a - // silent fallback to the build-time password of a different network. Copied out of prefs - // so 'set wifi.*' edits only take effect on reboot, as their replies promise. - // (No NULL-for-open-network: the RP2040 core does strlen() on the password unguarded.) + // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults if (the_mesh.getNodePrefs()->wifi_ssid[0]) { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); @@ -239,10 +235,9 @@ void setup() { WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); #if defined(RP2040_PLATFORM) - // ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the + // the join itself blocks inside the core (CYW43::begin busy-waits for the // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the // extra DHCP wait. Give the first connect a full window, then bound the retries below. - // Upgrade path if the stall ever matters: run WiFi on core1. WiFi.beginNoBlock(wifi_ssid, wifi_pwd); last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry #else From cee43752c7bd8adb44aac5431e9d3350d6ebba17 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 8 Sep 2026 22:54:50 +1200 Subject: [PATCH 109/174] add new ENABLE_WIFI_INTERFACE build flag for companions without providing ssid and pwd --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/NodePrefs.h | 11 +++++++---- examples/companion_radio/main.cpp | 14 ++++++++++---- examples/companion_radio/ui-new/UITask.cpp | 4 ++-- examples/companion_radio/ui-tiny/UITask.cpp | 4 ++-- variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/lilygo_tbeam_1w/platformio.ini | 1 + .../lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/meshnology_w12/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 4 ++-- variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 27 files changed, 51 insertions(+), 17 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7a380e98..2d7d1491 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,7 +1,7 @@ #include "MyMesh.h" #include // needed for PlatformIO -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif #include @@ -2160,7 +2160,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE if (memcmp(command, "set wifi.ssid ", 14) == 0) { StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); savePrefs(); @@ -2438,7 +2438,7 @@ void MyMesh::loop() { checkCLIRescueCmd(); } else { checkSerialInterface(); -#if defined(WIFI_SSID) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) +#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE) // RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a // display + long-press), so serve config commands on the otherwise unused USB serial checkCLIRescueCmd(); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ed408f9a..cf1914d0 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -44,7 +44,10 @@ public: char default_scope_name[31]; uint8_t default_scope_key[16]; int8_t tz_offset = 0; -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used char wifi_pwd[64] = {0}; uint8_t wifi_enabled = 1; // enabled by default to allow wifi only builds to work. wifi won't be started if ssid is empty @@ -167,7 +170,7 @@ private: DynamicConfigSerializer custom; -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE class WiFiPrefs : public ConfigSerializer { NodePrefs* _parent; protected: @@ -194,13 +197,13 @@ protected: def("repeat", repeat); def("comp", companion); def("custom", custom); -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE def("wifi", wifi); #endif } public: NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE , wifi(this) #endif { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d886b804..6dc9acdc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -36,7 +36,13 @@ MultiSerialInterface interface_manager; #endif // include wifi interface -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif + #ifndef WIFI_PWD + #define WIFI_PWD "" + #endif #ifndef TCP_PORT #define TCP_PORT 5000 #endif @@ -121,7 +127,7 @@ void halt() { } /* WIFI RECONNECT TRACKERS */ -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set @@ -208,7 +214,7 @@ void setup() { #endif // add wifi interface -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults if (the_mesh.getNodePrefs()->wifi_ssid[0]) { strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); @@ -302,7 +308,7 @@ void loop() { #endif } -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE if (wifi_enabled) { // RP2040 has no WiFi event callbacks, so poll the link state instead #if defined(RP2040_PLATFORM) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 2c79ab91..53ae480c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -3,7 +3,7 @@ #include "../MyMesh.h" #include "target.h" #include -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -260,7 +260,7 @@ public: sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year()); display.drawTextCentered(display.width() / 2, 80, tmp); #endif - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index b6bdbcf4..9ecd66f1 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -4,7 +4,7 @@ #include "target.h" #include "u8g2_icons.h" -#ifdef WIFI_SSID +#ifdef ENABLE_WIFI_INTERFACE #include #endif @@ -173,7 +173,7 @@ public: display.setCursor(0, 19); display.print(tmp); - #ifdef WIFI_SSID + #ifdef ENABLE_WIFI_INTERFACE IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index df986cf0..2f07c021 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -173,6 +173,7 @@ extends = Heltec_RC32 build_flags = ${Heltec_RC32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -315,6 +316,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 178508d8..5fd9edab 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -182,6 +182,7 @@ extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 28e80554..6435936e 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -178,6 +178,7 @@ extends = Heltec_lora32_v2 build_flags = ${Heltec_lora32_v2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 27541a8a..a2b159ec 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -185,6 +185,7 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display @@ -341,6 +342,7 @@ lib_deps = extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 25f9ee3b..96b3eec0 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -228,6 +228,7 @@ extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -394,6 +395,7 @@ extends = heltec_v4_tft build_flags = ${heltec_v4_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 8c2feb94..7611cc1c 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -175,6 +175,7 @@ extends = heltec_v4_r8_oled build_flags = ${heltec_v4_r8_oled.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 @@ -301,6 +302,7 @@ extends = heltec_v4_r8_tft build_flags = ${heltec_v4_r8_tft.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 16db0257..ab9c9e67 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -153,6 +153,7 @@ extends = LilyGo_TBeam_1W build_flags = ${LilyGo_TBeam_1W.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index a9991f25..af4a8eaf 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -148,6 +148,7 @@ extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index cf24a5d5..dc60b6f7 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -132,6 +132,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D WIFI_SSID='"ssid"' diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index c0e8c80d..ffab9d57 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -171,6 +171,7 @@ extends = meshnology_w12 build_flags = ${meshnology_w12.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 3c5049e0..0f8d31de 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -143,6 +143,7 @@ extends = nibble_screen_connect_base build_flags = ${nibble_screen_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 9789eabf..c7e7b076 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -140,6 +140,7 @@ extends = nibble_zero_connect_base build_flags = ${nibble_zero_connect_base.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 2fee8c26..5f3b905b 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -172,6 +172,7 @@ extends = rak3112 build_flags = ${rak3112.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 763c96bc..9814f397 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -90,12 +90,11 @@ build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D ENABLE_USB_INTERFACE + -D ENABLE_WIFI_INTERFACE -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH -D BLE_PIN_CODE=123456 ; NOTE: DO NOT ENABLE --> -D BLE_DEBUG_LOGGING=1 (shares Serial with the USB interface) ; NOTE: DO NOT ENABLE --> -D WIFI_DEBUG_LOGGING=1 - -D WIFI_SSID='""' ; no default network, configure with 'set wifi.ssid' / 'set wifi.pwd' - -D WIFI_PWD='""' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} @@ -108,6 +107,7 @@ lib_deps = ${rpi_picow.lib_deps} [env:PicoW_companion_radio_wifi] extends = rpi_picow build_flags = ${rpi_picow.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index d8bdd5e8..c1ae765f 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -224,6 +224,7 @@ extends = Station_G2 build_flags = ${Station_G2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 477c135b..7006d939 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -139,6 +139,7 @@ extends = Station_G3_ESP32 build_flags = ${Station_G3_ESP32.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index c1f5612a..862c7154 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -176,6 +176,7 @@ extends = ThinkNode_M2 build_flags = ${ThinkNode_M2.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index f64e4e77..a3f348b3 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -190,6 +190,7 @@ extends = ThinkNode_M5 build_flags = ${ThinkNode_M5.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 508fc813..5606d482 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -135,6 +135,7 @@ extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} -I examples/companion_radio/ui-orig + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=NullDisplayDriver diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index 085ab7d5..d4983491 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -137,6 +137,7 @@ extends = ThinkNode_M9 build_flags = ${ThinkNode_M9.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index cca2907a..62e6fd21 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -118,6 +118,7 @@ build_src_filter = ${Xiao_esp32_C3.build_src_filter} + build_flags = ${Xiao_esp32_C3.build_flags} + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index de656c44..3bb95607 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -208,6 +208,7 @@ build_src_filter = ${Xiao_S3_WIO.build_src_filter} build_flags = ${Xiao_S3_WIO.build_flags} -I examples/companion_radio/ui-new + -D ENABLE_WIFI_INTERFACE -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 From 82ea992d869f8ac1a5c1af32a24540978a8d7729 Mon Sep 17 00:00:00 2001 From: Yoshi Walsh Date: Wed, 9 Sep 2026 17:42:35 +1000 Subject: [PATCH 110/174] Fix Git error during project configuration Previously project configuration would fail with "fatal: couldn't find remote ref d541301" (git v2.55.0) Per https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-refspec, the needs to be either a ref or a "**fully spelled** hex object name" This requirement is also mentioned in this SO answer: https://stackoverflow.com/a/30701724 After this change, project configuration now succeeds. 219812 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 2219c978..de4d6c29 100644 --- a/platformio.ini +++ b/platformio.ini @@ -85,7 +85,7 @@ platform_packages = ; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect ; https://github.com/meshcore-dev/MeshCore/pull/1177 ; https://github.com/meshcore-dev/MeshCore/pull/1295 - framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301 + framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301665b40959682252911e57b11df3ee651a platformio/toolchain-gccarmnoneeabi@^1.140201.0 extra_scripts = create-uf2.py build_flags = ${arduino_base.build_flags} From 0a82fcd20da3f928a7e8e4d0b4516980bead2a7a Mon Sep 17 00:00:00 2001 From: tekk Date: Wed, 9 Sep 2026 22:10:41 +0200 Subject: [PATCH 111/174] fix(companion): gate CMD_SEND_CHANNEL_DATA payload on MAX_GROUP_DATA_LENGTH Payloads of 166 or 167 bytes previously passed the frame-sized bound (MAX_CHANNEL_DATA_LENGTH = 167) but were rejected by sendGroupData against MAX_GROUP_DATA_LENGTH (165), causing ERR_CODE_TABLE_FULL (retry later) to be returned instead of ERR_CODE_ILLEGAL_ARG. Fixes #3345 --- examples/companion_radio/MyMesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 317cf284..d96e0f76 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1270,8 +1270,8 @@ void MyMesh::handleCmdFrame(size_t len) { writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx } else if (data_type == DATA_TYPE_RESERVED) { writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else if (payload_len > MAX_CHANNEL_DATA_LENGTH) { - MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH); + } else if (payload_len > MAX_GROUP_DATA_LENGTH) { + MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH); writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) { writeOKFrame(); From f722794e50495bd19a5a9f9fb93ed801c445574d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Thu, 10 Sep 2026 09:17:37 +0100 Subject: [PATCH 112/174] Validate set af input to prevent out-of-range airtime factors --- src/helpers/CommonRadioPrefs.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index a25df880..9d92cad9 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -67,8 +67,14 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } if (memcmp(command, "set af ", 7) == 0) { - setAirtimeFactor(atof(&command[7])); - strcpy(reply, "OK"); + char* end; + float af = strtof(&command[7], &end); + if (end == &command[7] || af < 0 || af > 9) { + strcpy(reply, "ERROR: af must be 0-9"); + } else { + setAirtimeFactor(af); + strcpy(reply, "OK"); + } return true; } From d69ae98b5a2db3b7484183d5acaac8b3d550f3ff Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 12 Sep 2026 16:53:45 +1000 Subject: [PATCH 113/174] * telemHasChanged() mod, so that only _specified_ min_delta values are considered. --- examples/simple_sensor/SensorMesh.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index de58e2b1..4f2ed8bf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -81,7 +81,7 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) { /* --------------------- Cayenne LPP helpers ----------------------------*/ -static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) { +static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) { uint8_t i = 0; while (i + 2 < size) { @@ -96,7 +96,7 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u } i += sz; // skip } - return 0.0f; // not found + return def_value; // not found } /* ------------------ end Cayenne LPP helpers ----------------------*/ @@ -117,12 +117,14 @@ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_l float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = findTelemValue(min_deltas, min_deltas_len, ch, t); + float min_delta = min_deltas_len > 0 + ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG + : 0.0f; // for ANY change if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes + return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -961,7 +963,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) { } float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { - return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type); + return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f); } bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { From 21cdd7b319c0145ebc25dcd05eec34341434a798 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:38:55 -0700 Subject: [PATCH 114/174] Add Muzi Base board support (Duo + Uno + SuperIO) One nRF52840 variant shared across both radios, picked per env: Duo runs the LR1121, Uno the SX1262. Same PCB, only the radio and its DIO1 pin change. SuperIO adds the SH1107 OLED, GPS, buzzer and joystick. GPS is driven by the 3-position mode switch (Mode 2 = on), polled live the way thinknode_m1 does it. Five quick user-button presses power the unit off; a press wakes it back on (arms the button as an nRF52 GPIO SENSE source before SYSTEMOFF). Consolidates #2054 and andyshinn's shared-base/uno work, rebased on dev. Co-authored-by: lbibass Co-authored-by: Andy Shinn --- boards/muzi_base.json | 72 +++++ src/helpers/radiolib/CustomLR1121.h | 41 +++ src/helpers/radiolib/CustomLR1121Wrapper.h | 50 ++++ src/helpers/ui/SH1107Display.cpp | 113 +++++++ src/helpers/ui/SH1107Display.h | 43 +++ variants/muzi_base/muzi_baseBoard.cpp | 52 ++++ variants/muzi_base/muzi_baseBoard.h | 48 +++ variants/muzi_base/platformio.ini | 326 +++++++++++++++++++++ variants/muzi_base/target.cpp | 162 ++++++++++ variants/muzi_base/target.h | 64 ++++ variants/muzi_base/variant.cpp | 90 ++++++ variants/muzi_base/variant.h | 169 +++++++++++ 12 files changed, 1230 insertions(+) create mode 100644 boards/muzi_base.json create mode 100644 src/helpers/radiolib/CustomLR1121.h create mode 100644 src/helpers/radiolib/CustomLR1121Wrapper.h create mode 100644 src/helpers/ui/SH1107Display.cpp create mode 100644 src/helpers/ui/SH1107Display.h create mode 100644 variants/muzi_base/muzi_baseBoard.cpp create mode 100644 variants/muzi_base/muzi_baseBoard.h create mode 100644 variants/muzi_base/platformio.ini create mode 100644 variants/muzi_base/target.cpp create mode 100644 variants/muzi_base/target.h create mode 100644 variants/muzi_base/variant.cpp create mode 100644 variants/muzi_base/variant.h diff --git a/boards/muzi_base.json b/boards/muzi_base.json new file mode 100644 index 00000000..1b2cf7df --- /dev/null +++ b/boards/muzi_base.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "muzi_base", + "mcu": "nrf52840", + "variant": "MUZI_BASE", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "Muzi Base", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/muzi-works", + "vendor": "MuziWorks" +} diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h new file mode 100644 index 00000000..873bfabc --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "MeshCore.h" + +class CustomLR1121 : public LR1121 { + bool _rx_boosted = false; + + public: + CustomLR1121(Module *mod) : LR1121(mod) { } + + size_t getPacketLength(bool update) override { + size_t len = LR1121::getPacketLength(update); + if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { + // we've just received a corrupted packet + // this may have triggered a bug causing subsequent packets to be shifted + // call standby() to return radio to known-good state + // recvRaw will call startReceive() to restart rx + MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()"); + standby(); + } + return len; + } + + float getFreqMHz() const { return freqMHz; } + + int16_t setRxBoostedGainMode(bool en) { + _rx_boosted = en; + return LR1121::setRxBoostedGainMode(en); + } + + bool getRxBoostedGainMode() const { return _rx_boosted; } + + bool isReceiving() { + uint16_t irq = getIrqStatus(); + bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); + return detected; + } + uint8_t getSpreadingFactor() const { return spreadingFactor; } + +}; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h new file mode 100644 index 00000000..5361ee23 --- /dev/null +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CustomLR1121.h" +#include "RadioLibWrappers.h" +#include "LR11x0Reset.h" + +class CustomLR1121Wrapper : public RadioLibWrapper { +public: + CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + + void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + ((CustomLR1121 *)_radio)->setFrequency(freq); + ((CustomLR1121 *)_radio)->setSpreadingFactor(sf); + ((CustomLR1121 *)_radio)->setBandwidth(bw); + ((CustomLR1121 *)_radio)->setCodingRate(cr); + updatePreamble(sf); + } + + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } + bool isReceivingPacket() override { + return ((CustomLR1121 *)_radio)->isReceiving(); + } + float getCurrentRSSI() override { + float rssi = -110; + ((CustomLR1121 *)_radio)->getRssiInst(&rssi); + return rssi; + } + + void onSendFinished() override { + RadioLibWrapper::onSendFinished(); + _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts + } + + uint32_t getEstAirtimeFor(int len_bytes) override { + auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes); + return airtime < 200 ? 200 : airtime; // at least 200 millis + } + + float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); } + + uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); } + + bool setRxBoostedGainMode(bool en) override { + return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; + } + bool getRxBoostedGainMode() const override { + return ((CustomLR1121 *)_radio)->getRxBoostedGainMode(); + } +}; diff --git a/src/helpers/ui/SH1107Display.cpp b/src/helpers/ui/SH1107Display.cpp new file mode 100644 index 00000000..09bf424f --- /dev/null +++ b/src/helpers/ui/SH1107Display.cpp @@ -0,0 +1,113 @@ +#include "SH1107Display.h" +#include +#include "Adafruit_SH110X.h" + +#ifndef DISPLAY_ROTATION +#define DISPLAY_ROTATION 0 +#endif + +ColorVal UIColor::window_bkg = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_BLACK; +ColorVal UIColor::title_txt = SH110X_WHITE; +ColorVal UIColor::primary_txt = SH110X_WHITE; +ColorVal UIColor::secondary_txt = SH110X_WHITE; +ColorVal UIColor::warning_txt = SH110X_WHITE; +ColorVal UIColor::popup_bkg = SH110X_BLACK; +ColorVal UIColor::popup_txt = SH110X_WHITE; +ColorVal UIColor::corp_blue = SH110X_WHITE; + +bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr) +{ + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +} + +bool SH1107Display::begin() +{ + bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + if (result) { + display.setRotation(DISPLAY_ROTATION); + } + return result; +} + +void SH1107Display::turnOn() +{ + display.oled_command(SH110X_DISPLAYON); + uint8_t cmd[] = {0xD5, 0xF0}; + display.oled_commandList(cmd, 2); + _isOn = true; +} + +void SH1107Display::turnOff() +{ + display.oled_command(SH110X_DISPLAYOFF); + _isOn = false; +} + +void SH1107Display::clear() +{ + display.clearDisplay(); + display.display(); +} + +void SH1107Display::startFrame(ColorVal bkg) +{ + display.clearDisplay(); // TODO: apply 'bkg' + display.setContrast(120); // 0-127. default setting was causing some flickering. + // display.SH110X_SETPRECHARGE(255); + _color = SH110X_WHITE; + display.setTextColor(_color); + display.setTextSize(1); + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void SH1107Display::setTextSize(int sz) +{ + display.setTextSize(sz); +} + +void SH1107Display::setColor(ColorVal c) +{ + _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + display.setTextColor(_color); +} + +void SH1107Display::setCursor(int x, int y) +{ + display.setCursor(x, y); +} + +void SH1107Display::print(const char *str) +{ + display.print(str); +} + +void SH1107Display::fillRect(int x, int y, int w, int h) +{ + display.fillRect(x, y, w, h, _color); +} + +void SH1107Display::drawRect(int x, int y, int w, int h) +{ + display.drawRect(x, y, w, h, _color); +} + +void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) +{ + display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); +} + +uint16_t SH1107Display::getTextWidth(const char *str) +{ + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + +void SH1107Display::endFrame() +{ + display.display(); +} diff --git a/src/helpers/ui/SH1107Display.h b/src/helpers/ui/SH1107Display.h new file mode 100644 index 00000000..417184c2 --- /dev/null +++ b/src/helpers/ui/SH1107Display.h @@ -0,0 +1,43 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#define SH110X_NO_SPLASH +#include + +#ifndef PIN_OLED_RESET +#define PIN_OLED_RESET -1 +#endif + +#ifndef DISPLAY_ADDRESS +#define DISPLAY_ADDRESS 0x3c +#endif + +class SH1107Display : public DisplayDriver +{ + Adafruit_SH1107 display; + bool _isOn; + uint8_t _color; + + bool i2c_probe(TwoWire &wire, uint8_t addr); + +public: + SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; } + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; + void setTextSize(int sz) override; + void setColor(ColorVal c) override; + void setCursor(int x, int y) override; + void print(const char *str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; + uint16_t getTextWidth(const char *str) override; + void endFrame() override; +}; diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/muzi_baseBoard.cpp new file mode 100644 index 00000000..ed713fa0 --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include "muzi_baseBoard.h" + +#ifdef NRF52_POWER_MANAGEMENT +const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +void muzi_baseBoard::initiateShutdown(uint8_t reason) { + // Disable LoRa module power before shutdown + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || + reason == SHUTDOWN_REASON_BOOT_PROTECT) { + configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + } + + enterSystemOff(reason); +} +#endif // NRF52_POWER_MANAGEMENT + +void muzi_baseBoard::begin() { + NRF52BoardDCDC::begin(); + pinMode(PIN_VBAT_READ, INPUT); +#ifdef muzi_base_superIO + // 12V rail is only needed for the superIO display + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display + delay(250); +#endif + Wire.begin(); + // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. + +#ifdef PIN_USER_BTN + pinMode(PIN_USER_BTN, INPUT_PULLUP); +#endif + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + +// #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) +// Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +// #endif +#ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); +#endif +} diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/muzi_baseBoard.h new file mode 100644 index 00000000..e5412b6a --- /dev/null +++ b/variants/muzi_base/muzi_baseBoard.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +// The Muzi Base PCB ships in two radio flavors. Keep the user-facing identity +// (OTA/DFU name and reported manufacturer) distinct per radio even though the +// board logic is shared. +#if defined(USE_SX1262) + #define MUZI_BASE_OTA_NAME "MUZI_BASE_UNO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Uno" +#else + #define MUZI_BASE_OTA_NAME "MUZI_BASE_DUO_OTA" + #define MUZI_BASE_MFR_NAME "Muzi Base Duo" +#endif + +class muzi_baseBoard : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + +public: + muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + void begin(); + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(1); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + // ADC_MULTIPLIER is the voltage divider ratio + return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return MUZI_BASE_MFR_NAME; + } +}; diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini new file mode 100644 index 00000000..eb35d569 --- /dev/null +++ b/variants/muzi_base/platformio.ini @@ -0,0 +1,326 @@ +; ============================================================================ +; Muzi Base (MuziWorks) +; +; One nRF52840 board, shipped in two radio flavors. The radio is selected per +; environment; all board logic lives in variants/muzi_base and is shared: +; muzi_base_duo_* -> LR1121 (USE_LR1121) +; muzi_base_uno_* -> SX1262 (USE_SX1262) +; ============================================================================ + +[muzi_base_common] +extends = nrf52_base +board = muzi_base +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/muzi_base + -I src/helpers/ui + -D muzi_base + -D NRF52_POWER_MANAGEMENT + -D PIN_USER_BTN=PIN_BUTTON1 + -D USER_BTN_PRESSED=LOW + -D PIN_STATUS_LED=35 + -D LORA_TX_POWER=22 + -D P_LORA_BUSY=LORA_BUSY + -D P_LORA_SCLK=LORA_SCLK + -D P_LORA_NSS=LORA_NSS + -D P_LORA_DIO_1=LORA_DIO_1 + -D P_LORA_MISO=LORA_MISO + -D P_LORA_MOSI=LORA_MOSI + -D P_LORA_RESET=LORA_RESET + -D QSPIFLASH=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + +<../variants/muzi_base> +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = + ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + +; --------------------------------------------------------------------------- +; Radio selection (Duo = LR1121, Uno = SX1262). The matching radio pins / TCXO +; / RF-switch defines are picked up in variant.h via USE_LR1121 / USE_SX1262. +; --------------------------------------------------------------------------- +[muzi_base_duo] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_LR1121 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=3.0 + +[muzi_base_uno] +extends = muzi_base_common +build_flags = ${muzi_base_common.build_flags} + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + +; =========================================================================== +; Duo (LR1121) environments +; =========================================================================== +[env:muzi_base_duo_repeater] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_room_server] +extends = muzi_base_duo +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_duo.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_duo.lib_deps} + +[env:muzi_base_duo_companion_radio_usb] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_duo.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_duo_companion_radio_ble] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_duo_superIO] +extends = muzi_base_duo +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_duo.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_duo.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_duo_companion_radio_ble_superIO] +extends = muzi_base_duo_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_duo_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_duo_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_duo_superIO.lib_deps} + +; =========================================================================== +; Uno (SX1262) environments +; =========================================================================== +[env:muzi_base_uno_repeater] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_room_server] +extends = muzi_base_uno +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D ADVERT_NAME='"Muzi Base Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${muzi_base_uno.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${muzi_base_uno.lib_deps} + +[env:muzi_base_uno_companion_radio_usb] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${muzi_base_uno.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:muzi_base_uno_companion_radio_ble] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + ; -D ADVERT_NAME='"@@MAC"' +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[muzi_base_uno_superIO] +extends = muzi_base_uno +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno.build_flags} + -D muzi_base_superIO + -D UI_HAS_JOYSTICK=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1107Display + -D DISPLAY_ROTATION=2 + -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT + -D PIN_BUZZER=22 +build_src_filter = ${muzi_base_uno.build_src_filter} + + + + + + + + + + +lib_deps = ${muzi_base_uno.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SH110X@^2.1.14 + artronshop/ArtronShop_RX8130CE@1.0.0 + adafruit/Adafruit GFX Library @ ^1.12.1 +debug_tool = jlink +upload_protocol = nrfutil + +[env:muzi_base_uno_companion_radio_ble_superIO] +extends = muzi_base_uno_superIO +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${muzi_base_uno_superIO.build_flags} + -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_GROUP_CHANNELS=40 + -I examples/companion_radio/ui-new + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 + -D QSPIFLASH=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${muzi_base_uno_superIO.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${muzi_base_uno_superIO.lib_deps} diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp new file mode 100644 index 00000000..2af0c710 --- /dev/null +++ b/variants/muzi_base/target.cpp @@ -0,0 +1,162 @@ +#include +#include +#include "target.h" +#include "variant.h" + +muzi_baseBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + MuziBaseSensorManager sensors = MuziBaseSensorManager(nmea); +#else + MuziBaseSensorManager sensors; +#endif + +// user button for the N-click power-off. single-click mode (multiclick off) so +// each press is its own CLICK; we count them ourselves. reliable across the +// idle-sleep loop because it's the same MomentaryButton the UI navigates with. +static MomentaryButton pwr_btn(PIN_USER_BTN, 0, true, true, false); + +// power off, arming the user button as a wake source first so a later press +// turns it back on. wait for the button to release (the 5th click already fires +// on release, so this returns quickly), then set nRF52 GPIO SENSE (press = LOW); +// it survives into SYSTEMOFF because shutdownPeripherals() doesn't touch this pin. +static void muziPowerOff() { + uint32_t t0 = millis(); + while (digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED && (millis() - t0) < 3000) { delay(5); } + delay(50); // settle + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + board.powerOff(); // shutdownPeripherals() + SYSTEMOFF +} + +bool MuziBaseSensorManager::begin() { + pwr_btn.begin(); + bool ok = EnvironmentSensorManager::begin(); +#if ENV_INCLUDE_GPS + pinMode(PIN_GPS_SWITCH, INPUT); + _last_gps_sw = digitalRead(PIN_GPS_SWITCH); // initial gps state from the switch + if (_last_gps_sw == HIGH) start_gps(); else stop_gps(); +#endif + return ok; +} + +void MuziBaseSensorManager::loop() { + // user button: USER_BTN_POWER_OFF_CLICKS quick clicks -> power off + if (pwr_btn.check() == BUTTON_EVENT_CLICK) { + unsigned long t = millis(); + if (t - _pwr_last_click > 2000) _pwr_clicks = 0; // restart if too slow + _pwr_last_click = t; + if (++_pwr_clicks >= USER_BTN_POWER_OFF_CLICKS) muziPowerOff(); + } +#if ENV_INCLUDE_GPS + unsigned long now = millis(); + if (now > _next_sw_check) { // check the mode switch ~once a sec + _next_sw_check = now + 1000; + int sw = digitalRead(PIN_GPS_SWITCH); + if (sw != _last_gps_sw) { + _last_gps_sw = sw; + if (sw == HIGH) start_gps(); else stop_gps(); + } + } +#endif + EnvironmentSensorManager::loop(); +} + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false); + MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false); + MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false); + MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true); +#endif + +#if defined(USE_LR1121) + #ifndef LORA_CR + #define LORA_CR 5 + #endif + + #ifdef RF_SWITCH_TABLE + static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC + }; + + static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, + }; + #endif +#endif // USE_LR1121 + +bool radio_init() { + //rtc_clock.begin(Wire); + +#if defined(USE_LR1121) + #ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; + #else + float tcxo = 1.6f; + #endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + + #ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); + #endif + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + return true; // success +#else // USE_SX1262 + // CustomSX1262::std_init() configures the SPI pins, runs begin() with the + // TCXO-voltage fallback, sets CRC, current limit, the DIO2 RF switch, and + // RX boosted gain from the SX126X_* build flags. + return radio.std_init(&SPI); +#endif +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h new file mode 100644 index 00000000..cbcfff14 --- /dev/null +++ b/variants/muzi_base/target.h @@ -0,0 +1,64 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "muzi_baseBoard.h" +#if defined(USE_LR1121) + #include +#elif defined(USE_SX1262) + #include +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif +#include +#include +#include +#include +#include // Added: Include for EnvironmentSensorManager +#include + + +#ifdef muzi_base_superIO + #include + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; + extern MomentaryButton joystick_left; + extern MomentaryButton joystick_right; + extern MomentaryButton back_btn; +#elif defined(DISPLAY_CLASS) + #include "helpers/ui/NullDisplayDriver.h" + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern muzi_baseBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +// muzi sensor manager, polled from main loop via sensors.loop(): +// - user button: USER_BTN_POWER_OFF_CLICKS quick presses -> power off (all builds) +// - gps mode switch on/off (superIO only, see thinknode_m1) +class MuziBaseSensorManager : public EnvironmentSensorManager { + unsigned long _pwr_last_click = 0; + uint8_t _pwr_clicks = 0; +#if ENV_INCLUDE_GPS + int _last_gps_sw = -1; + unsigned long _next_sw_check = 0; +#endif +public: +#if ENV_INCLUDE_GPS + MuziBaseSensorManager(LocationProvider& location) : EnvironmentSensorManager(location) {} +#else + MuziBaseSensorManager() {} +#endif + bool begin() override; + void loop() override; +}; +extern MuziBaseSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp new file mode 100644 index 00000000..b179af8a --- /dev/null +++ b/variants/muzi_base/variant.cpp @@ -0,0 +1,90 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, +// P1 pins. + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, +}; + +void initVariant() +{ + // All pins output HIGH by default. + // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 + + pinMode(PIN_VBAT_READ, INPUT); + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(PIN_BUTTON1, INPUT); + pinMode(PIN_BUTTON2, INPUT); + pinMode(PIN_BUTTON3, INPUT); + pinMode(PIN_BUTTON4, INPUT); + pinMode(PIN_BUTTON5, INPUT); + pinMode(PIN_BUTTON6, INPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_PIN, LOW); + digitalWrite(LED_BLUE, LOW); + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + + pinMode(SCREEN_12V_ENABLE, OUTPUT); + digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. +} diff --git a/variants/muzi_base/variant.h b/variants/muzi_base/variant.h new file mode 100644 index 00000000..b007c1f8 --- /dev/null +++ b/variants/muzi_base/variant.h @@ -0,0 +1,169 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_VBAT_READ (31) // P0.31 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.537 +#define ADC_RESOLUTION 14 +#define PIN_BATTERY_CHARGING (32+2) // P1.02 STAT2 +#define PIN_CHARGER_FAULT (27) // P0.27 STAT1 this pin is disabled on meshtastic. +// BQ25185 has 2 status pins: STAT1 and STAT2. Both are high when not charging. STAT1 high, STAT2 low: charging. Recoverable fault: STAT1 low, STAT2 high. Unrecoverable fault: both low. +// We only need to detect charging vs not charging, but someone else can use the fault pin to log when the battery gets too hot or cold. + +// Power management boot protection threshold (millivolts) +#define PWRMGT_VOLTAGE_BOOTLOCK 3100 // Won't boot below this voltage (mV). BB15 battery min voltage is 3v, 3100mV is minimum batt in meshtastic code. + +// LPCOMP wake configuration (voltage recovery from SYSTEMOFF) +#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT_READ +#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V) was the default on RAK4631. should still apply here. + +// Other pins +#define PIN_AREF (-1) +#define SCREEN_12V_ENABLE (23) // SH1107 OLED controller has a pin that needs to be enabled to turn on the screen. + +static const uint8_t AREF = (PIN_AREF); // not used + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (19) // P0.19 used for GPS RX +#define PIN_SERIAL1_TX (20) // P0.20 used for GPS TX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE1_SDA (4) // P0.4 +#define PIN_WIRE1_SCL (6) // P0.6 +#define PIN_WIRE_SDA (24) // P0.24 OLED I2C +#define PIN_WIRE_SCL (25) // P0.25 OLED I2C +#define I2C_NO_RESCAN +// #define I2C_NO_RESCAN +// #define HAS_QMA6100P +// #define QMA_6100P_INT_PIN (34) // P1.2 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (32+15) // internally connected to p1.15 +#define PIN_SPI_MOSI (32+14) // internally connected to p1.14 +#define PIN_SPI_SCK (32+13) // internally connected to p1.13 +#define PIN_SPI_NSS (32+12) // internally connected to p1.12 + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BUILTIN (35) +#define LED_BLUE (-1) // P1.04 turned off, because the blue LED was annoying. +// #define LED_GREEN (35) // P1.03 +#define LED_PIN LED_BUILTIN + +#define LED_STATE_ON LOW + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons +#define PIN_BUTTON1 (10) // P0.10 Menu / User Button | on superIO, this is in the center of the "D-Pad", but it's also the button on the Uno/Duo. +#define PIN_BUTTON2 (21) // Joystick Up +#define PIN_BUTTON3 (17) // Joystick Down +#define PIN_BUTTON4 (37) // Joystick Left +#define PIN_BUTTON5 (16) // Joystick Right +#define PIN_BUTTON6 (15) // Back / Cancel Button. +#define JOYSTICK_PRESS PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define PIN_BACK_BTN PIN_BUTTON6 + +// quick user-button presses that power the device off (per Muzi's docs) +#ifndef USER_BTN_POWER_OFF_CLICKS +#define USER_BTN_POWER_OFF_CLICKS 5 +#endif + +//////////////////////////////////////////////////////////////////////////////// +// LoRa radio +// +// The Muzi Base PCB is populated with one of two radios: +// * Base Uno -> SX1262 (USE_SX1262) +// * Base Duo -> LR1121 (USE_LR1121) +// Both share NSS/SCLK/MISO/MOSI/RESET/BUSY. The only pin difference is the +// radio IRQ (DIO1): the SX1262 routes it to P1.06, the LR1121 to P1.08. +// (see Meshtastic muzi_base variant, which defines both radios on this board) + +#define LORA_NSS (PIN_SPI_NSS) // P1.12 +#define LORA_RESET (32+10) // P1.10 +#define LORA_BUSY (32+11) // P1.11 +#define LORA_SCLK (PIN_SPI_SCK) // P1.13 +#define LORA_MISO (PIN_SPI_MISO) // P1.15 +#define LORA_MOSI (PIN_SPI_MOSI) // P1.14 + +// only the IRQ pin differs per radio. rf-switch/tcxo defines are in platformio.ini +#if defined(USE_LR1121) + #define LORA_DIO_1 (32+8) // P1.08 LR1121 IRQ/DIO1 +#elif defined(USE_SX1262) + #define LORA_DIO_1 (32+6) // P1.06 SX1262 IRQ/DIO1 +#else + #error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)" +#endif + +//////////////////////////////////////////////////////////////////////////////// +// QSPI Flash +#define PIN_QSPI_SCK (0 + 3) +#define PIN_QSPI_CS (0 + 26) +#define PIN_QSPI_IO0 (0 + 30) +#define PIN_QSPI_IO1 (0 + 29) +#define PIN_QSPI_IO2 (0 + 28) +#define PIN_QSPI_IO3 (0 + 2) + +#define EXTERNAL_FLASH_DEVICES W25Q128JVPQ +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// GPS +#define HAS_GPS 1 +#define PIN_GPS_RX PIN_SERIAL1_RX +#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_EN (32+1) // P1.01 PWR_IO2 on schematic. gps power, driven by the sensor manager. + +// superIO 3-position mode switch (pins from the meshtastic muzi_base variant). +// "Mode 2" reads HIGH on PIN_GPS_SWITCH and turns the gps on. +#define SWITCH_MODE1 (32+9) // P1.09 +#define SWITCH_MODE2 (12) // P0.12 +#define PIN_GPS_SWITCH SWITCH_MODE2 + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define BUZZER_PIN (22) // P0.22 same load switch design as GPS_EN. From df82dc23390097e72fdef829d48a34b124cf1036 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:05:38 +1000 Subject: [PATCH 115/174] * min_deltas now mandatory --- examples/simple_sensor/SensorMesh.cpp | 49 ++++++++++++++------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 4f2ed8bf..54bce50d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -102,29 +102,24 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { - if (telemetry.getSize() != prev_telem_size) return true; - auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); uint8_t i = 0; - while (i + 2 < size) { - // Get channel # - uint8_t ch = buf[i++]; - // Get data type - uint8_t t = buf[i++]; + while (i + 2 < min_deltas_len) { + uint8_t ch = min_deltas[i++]; // Get channel # + uint8_t t = min_deltas[i++]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float v = LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float pv = LPPData::getFloat(&prev_telem[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); - float min_delta = min_deltas_len > 0 - ? findTelemValue(min_deltas, min_deltas_len, ch, t, 1.0e+16f) // default is just something BIG - : 0.0f; // for ANY change + float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + float v = findTelemValue(buf, size, ch, t, 0.0f); + float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); if (abs(v - pv) > min_delta) return true; // Yes, has changed i += sz; // skip } - return false; // no changes (OR none of the -specified- telemetry values changed by min_delta) + return false; // none of the -specified- telemetry values changed by min_delta } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -202,26 +197,34 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u uint16_t timeout_secs; memcpy(&timeout_secs, &payload[4], 2); uint8_t reserved = payload[6]; + uint8_t min_deltas_len = payload[7]; RegionEntry* r; if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope r = recv_pkt_region; } else { // use default scope r = region_map.getDefaultRegion(); } - from->extra.sensor.scope_region_id = r ? r->id : 0; - from->extra.sensor.expiry_timestamp = r ? getRTCClock()->getCurrentTime() + timeout_secs : 0; - from->extra.sensor.min_deltas_len = min(sizeof(from->extra.sensor.min_deltas), (size_t)payload[7]); - // NOTE: curr impl truncates LPP min_diffs spec (re-do if better impl is needed) - memcpy(from->extra.sensor.min_deltas, &payload[8], from->extra.sensor.min_deltas_len); - - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); // reply with actual expiry timestamp (or 0 for error) - strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used - return 8 + strlen((char *)&reply_data[8]); + uint8_t reply_len; + if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) { + from->extra.sensor.scope_region_id = r->id; + from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; + from->extra.sensor.min_deltas_len = min_deltas_len; + memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); + // reply with actual expiry timestamp + memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used + reply_len = 8 + strlen((char *)&reply_data[8]); + } else { + memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + reply_len = 8; + } + return reply_len; } if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { from->extra.sensor.scope_region_id = 0; from->extra.sensor.push_tag = 0; from->extra.sensor.expiry_timestamp = 0; + from->extra.sensor.min_deltas_len = 0; // REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE? memset(&reply_data[4], 0, 8); // success getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness @@ -1021,7 +1024,7 @@ void SensorMesh::loop() { // compare with previous telemetry, check if any deltas are greater than subscriber minimums for (int i = 0; i < acl.getNumClients(); i++) { auto c = acl.getClientByIdx(i); - if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0) continue; // skip deleted entries, or Not subscribed to deltas + if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired From c6b051d063c611f892fba0341919a969e36991a9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 13 Sep 2026 11:40:25 +1000 Subject: [PATCH 116/174] * request payload doco --- docs/payloads.md | 52 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/payloads.md b/docs/payloads.md index fb9cbaf9..493f628c 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | request sub type | | request data | rest of payload | application-defined request payload body | For the common chat/server helpers in `BaseChatMesh`, the current request type values are: @@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | Value | Name | Description | |--------|-----------|----------------------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | keep-alive request used for maintained connections | +| `0x02` | keepalive | (deprecated) | +| `0x03` | get telemetry | request node telemetry | +| `0x04` | get min/max/avg | get sensor node stats on time series data | +| `0x05` | get acl | node ACL query | +| `0x06` | get neighbors | node neighbors query | +| `0x08` | subscribe | subscribe to telemetry push | +| `0x09` | ubsubscribe | unsubscribe from telemetry push | #### Get stats @@ -125,21 +132,50 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -#### Get telemetry data - -Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. - #### Get Telemetry -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x03 (request sub type) | +| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) | #### Get Min/Max/Ave (Sensor nodes) -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x04 (request sub type) | +| start | 4 | starting time, seconds ago | +| end | 4 | ending time, seconds ago | +| reserved | 2 | should be zeroes | + +#### Subscribe to Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x08 (request sub type) | +| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads | +| timeout secs | 2 | subscription timeout (seconds) | +| reserved | 1 | should be zero | +| min deltas len | 1 | byte length of LPP encoded min_deltas | +| min deltas | (variable) | LPP encoded min_deltas | + +#### Unsubscribe from Telemetry push - (Sensor nodes) + +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x09 (request sub type) | #### Get Access List -Not defined in `BaseChatMesh`. +| Field | Size (bytes) | Description | +|----------------|--------------|------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| req type | 1 | 0x05 (request sub type) | +| reserved | 2 | should be zeroes | #### Get Neighbors From a88de69486bab69280f3ebb92e1732919c3bd61c Mon Sep 17 00:00:00 2001 From: TadyTheFish Date: Sun, 9 Aug 2026 21:27:22 +0200 Subject: [PATCH 117/174] Enable INA226 support for ProMicro nRF52840 --- variants/promicro/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 90cb475f..da27c2fa 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -23,6 +23,7 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_BMP280=1 -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 + -D ENV_INCLUDE_INA226=1 build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/promicro> @@ -34,6 +35,7 @@ lib_deps= ${nrf52_base.lib_deps} adafruit/Adafruit BME280 Library @ ^2.3.0 adafruit/Adafruit BMP280 Library@^2.6.8 stevemarple/MicroNMEA @ ^2.0.6 + robtillaart/INA226@^0.6.6 [env:ProMicro_repeater] extends = Promicro From bc8ccefafe1423ba50a0932da815a40c063e6121 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 14 Sep 2026 20:23:10 +1000 Subject: [PATCH 118/174] * fix for telemHasChanged() --- examples/simple_sensor/SensorMesh.cpp | 41 ++++++++++++++++++--------- examples/simple_sensor/SensorMesh.h | 4 +-- src/helpers/ClientACL.h | 1 + 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 54bce50d..70a619f7 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -101,25 +101,43 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u /* ------------------ end Cayenne LPP helpers ----------------------*/ -bool SensorMesh::telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len) { +bool SensorMesh::telemHasChanged(ClientInfo* c) { auto buf = telemetry.getBuffer(); uint8_t size = telemetry.getSize(); uint8_t i = 0; + bool changed = false; - while (i + 2 < min_deltas_len) { - uint8_t ch = min_deltas[i++]; // Get channel # - uint8_t t = min_deltas[i++]; // Get data type + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type uint8_t sz = LPPData::getDataSize(t); - float min_delta = LPPData::getFloat(&min_deltas[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); float v = findTelemValue(buf, size, ch, t, 0.0f); - float pv = findTelemValue(prev_telem, prev_telem_size, ch, t, 0.0f); - if (abs(v - pv) > min_delta) return true; // Yes, has changed + if (abs(v - pv) > min_delta) changed = true; // Yes, has changed - i += sz; // skip + i += 2 + sz; // skip } - return false; // none of the -specified- telemetry values changed by min_delta + if (changed) { + // take snapshot of all _monitored_ telem values, for next cycle + i = 0; + while (i + 2 < c->extra.sensor.min_deltas_len) { + uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel # + uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type + uint8_t sz = LPPData::getDataSize(t); + + c->extra.sensor.prev_telem[i] = ch; + c->extra.sensor.prev_telem[i + 1] = t; + + float v = findTelemValue(buf, size, ch, t, 0.0f); + LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t)); + + i += 2 + sz; // skip + } + } + return changed; } uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { @@ -774,7 +792,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; - prev_telem_size = 0; region_load_active = false; // defaults @@ -1028,7 +1045,7 @@ void SensorMesh::loop() { RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id); if (r == NULL) continue; // unknown region scope if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired - if (telemHasChanged(c->extra.sensor.min_deltas, c->extra.sensor.min_deltas_len)) { + if (telemHasChanged(c)) { TransportKey scope; if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) { uint8_t tlen = telemetry.getSize(); @@ -1048,8 +1065,6 @@ void SensorMesh::loop() { } } } - memcpy(prev_telem, telemetry.getBuffer(), telemetry.getSize()); // save snapshot for next compare cycle - prev_telem_size = telemetry.getSize(); onSensorDataRead(); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index e0e03c81..6b7a32fe 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -148,8 +148,6 @@ private: uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; CayenneLPP telemetry; - uint8_t prev_telem_size; - uint8_t prev_telem[MAX_PACKET_PAYLOAD - 4]; TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* recv_pkt_region; @@ -166,7 +164,7 @@ private: uint8_t pending_cr; bool region_load_active; - bool telemHasChanged(const uint8_t* min_deltas, uint8_t min_deltas_len); + bool telemHasChanged(ClientInfo* c); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 39e71f2c..9938bc1e 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,6 +34,7 @@ struct ClientInfo { uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber uint8_t min_deltas_len; uint8_t min_deltas[14]; // LPP encoded + uint8_t prev_telem[14]; // LPP encoded } sensor; } extra; From 61fcc49c8ae58cb1504be76fd9828fd4f7b30e0e Mon Sep 17 00:00:00 2001 From: Avamander Date: Mon, 14 Sep 2026 17:53:50 +0300 Subject: [PATCH 119/174] Fix Heltec Mesh Node T1 GNSS pins --- variants/heltec_t1/variant.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_t1/variant.h b/variants/heltec_t1/variant.h index b52aeba0..72b9a9ed 100644 --- a/variants/heltec_t1/variant.h +++ b/variants/heltec_t1/variant.h @@ -131,8 +131,8 @@ #define GPS_EN_ACTIVE LOW #define PIN_GPS_EN_ACTIVE LOW #define PIN_GPS_PPS (32 + 9) -#define GPS_TX_PIN (0 + 8) -#define GPS_RX_PIN (0 + 7) +#define GPS_RX_PIN (0 + 8) +#define GPS_TX_PIN (0 + 7) #define PIN_GPS_TX GPS_TX_PIN #define PIN_GPS_RX GPS_RX_PIN From 3819a0bd897b63e1ec8e11d0bb34d83a107ffb00 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Sep 2026 17:17:48 +1000 Subject: [PATCH 120/174] * subscribe response now returns expiry_secs (not timestamp) --- examples/simple_sensor/SensorMesh.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 70a619f7..b1860668 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -228,12 +228,13 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, u from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs; from->extra.sensor.min_deltas_len = min_deltas_len; memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len); - // reply with actual expiry timestamp - memcpy(&reply_data[4], &from->extra.sensor.expiry_timestamp, 4); + // reply with actual expiry seconds (we could modify/impose restriction) + memcpy(&reply_data[4], &timeout_secs, 2); + memset(&reply_data[6], 0, 2); // reserved strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used reply_len = 8 + strlen((char *)&reply_data[8]); } else { - memset(&reply_data[4], 0, 4); // expiry timestamp (0 for error) + memset(&reply_data[4], 0, 4); // expiry secs (0 for error) reply_len = 8; } return reply_len; From 3a3c13a143c9120bae648d1b5ddb8e12483e8695 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 15 Sep 2026 18:41:11 +1000 Subject: [PATCH 121/174] * refactor to support new Board::handleCommand(), and "get/set radio.fem.*" commands --- .../wio-tracker-l1-1w/WioTrackerL1Board.cpp | 51 ++++++++++++++++++- .../wio-tracker-l1-1w/WioTrackerL1Board.h | 10 ++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp index 8210c24e..c6dbdbf5 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.cpp @@ -15,7 +15,6 @@ void WioTrackerL1Board::begin() { pinMode(PIN_BUTTON4, INPUT_PULLUP); pinMode(PIN_BUTTON5, INPUT_PULLUP); pinMode(PIN_BUTTON6, INPUT_PULLUP); - #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); @@ -32,9 +31,57 @@ void WioTrackerL1Board::begin() { delay(10); // give sx1262 some time to power up } - + bool WioTrackerL1Board::setLoRaFemPaGainEnabled(bool enable) { _is_pa_enabled = enable; digitalWrite(SX126X_POWER_EN, enable ? HIGH : LOW); // enable/disable the 1W PA return true; } + +void WioTrackerL1Board::attachDynamicPrefs(KeyValueStore* prefs) { + _prefs = prefs; + + char gain[8]; + + gain[0] = 0; + _prefs->getByKey("fem_txgain", gain, 7); // get initial values + setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0); +} + +bool WioTrackerL1Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { + if (strcmp(command, "get radio.fem.rxgain") == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) { + strcpy(reply, "Error: unsupported"); + return true; + } + + if (strcmp(command, "get radio.fem.txgain") == 0) { + sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set radio.fem.txgain ", 21) == 0) { + if (memcmp(&command[21], "on", 2) == 0) { + if (setLoRaFemPaGainEnabled(true)) { + _prefs->setByKey("fem_txgain", "1"); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&command[21], "off", 3) == 0) { + if (setLoRaFemPaGainEnabled(false)) { + _prefs->setByKey("fem_txgain", "0"); + strcpy(reply, "OK - LoRa FEM TX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } + return true; + } + + return false; // not handled +} diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index c5a60944..798145db 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -8,10 +8,16 @@ class WioTrackerL1Board : public NRF52BoardDCDC { protected: uint8_t btn_prev_state; bool _is_pa_enabled = false; + KeyValueStore* _prefs = NULL; + + bool setLoRaFemPaGainEnabled(bool enable); + bool isLoRaFemPaGainEnabled() const { return _is_pa_enabled; } public: WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} void begin(); + void attachDynamicPrefs(KeyValueStore* prefs); + bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { @@ -35,10 +41,6 @@ public: return "Seeed Wio Tracker L1"; } - bool canControlLoRaFemPaGain() const override { return true; } - bool setLoRaFemPaGainEnabled(bool enable) override; - bool isLoRaFemPaGainEnabled() const override { return _is_pa_enabled; } - void powerOff() override { setLoRaFemPaGainEnabled(false); // turn Off PA NRF52Board::powerOff(); From 9d6d53a71a0f1a84cb337bdf6bff1d2497347feb Mon Sep 17 00:00:00 2001 From: Und3r_1337 Date: Wed, 16 Sep 2026 00:03:54 +0200 Subject: [PATCH 122/174] fix(config): allow digits in config keys so keys like gr_1hop round-trip --- src/helpers/ConfigSerializer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 36aff5cc..daf834b2 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -24,7 +24,9 @@ static bool is_whitespace(char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } static bool is_key_char(char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; + // digits are allowed: keys like gr_1hop contain one, otherwise loadSerial + // fails at that key and prefs stop reloading after a reboot + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } static bool is_value_char(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; From 3caf033de2fad1ce9c0b6f5d957cacfdb4460d26 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 16 Sep 2026 21:03:44 +1000 Subject: [PATCH 123/174] * refactor: removed UITask dependency from MyMesh. New MyMesh::Listener interface * UITask's are now a MyMesh task Listener --- examples/companion_radio/AbstractUITask.h | 11 ++--- examples/companion_radio/MyMesh.cpp | 48 +++++++-------------- examples/companion_radio/MyMesh.h | 14 ++++-- examples/companion_radio/main.cpp | 7 +-- examples/companion_radio/ui-new/UITask.cpp | 48 ++++++++++++++++----- examples/companion_radio/ui-new/UITask.h | 7 ++- examples/companion_radio/ui-orig/UITask.cpp | 47 +++++++++++++++++--- examples/companion_radio/ui-orig/UITask.h | 8 +++- examples/companion_radio/ui-tiny/UITask.cpp | 30 +++++++++++-- examples/companion_radio/ui-tiny/UITask.h | 7 ++- 10 files changed, 152 insertions(+), 75 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index b25b1442..d927c1a3 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -12,6 +12,7 @@ #endif #include "NodePrefs.h" +#include "MyMesh.h" enum class UIEventType { none, @@ -22,25 +23,21 @@ enum class UIEventType { ack }; -class AbstractUITask { +class AbstractUITask : public MyMesh::Listener { protected: mesh::MainBoard* _board; MultiSerialInterface* _interfaceManager; - bool _connected; AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { - _connected = false; } public: - void setHasConnection(bool connected) { _connected = connected; } - bool hasConnection() const { return _connected; } + bool hasConnection() const { return _interfaceManager->isConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); } void disableBluetooth() { _interfaceManager->disableBluetooth(); } - virtual void msgRead(int msgcount) = 0; - virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; + virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01a7473a..7becb0df 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -368,10 +368,6 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); } - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::newContactMessage); -#endif } // add inbound-path to mem cache @@ -395,6 +391,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path p->path_len = mesh::Packet::copyPath(p->path, path, path_len); } + if (_listener) _listener->onDiscoveredContact(contact, is_new, path_len, path); + if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] } @@ -520,16 +518,12 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS // we only want to show text messages on display, not cli data bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _ui) { - _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_serial->isConnected()) { - _ui->notify(UIEventType::contactMessage); - } + if (should_display && _listener) { + _listener->onMessageRecv(path_len, from.name, text); + _listener->onQueueSizeChanged(offline_queue_len); } -#endif } bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { @@ -640,20 +634,16 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); - } else { -#ifdef DISPLAY_CLASS - if (_ui) _ui->notify(UIEventType::channelMessage); -#endif } -#ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; + if (_listener) { + // Get the channel name from the channel index + ChannelDetails channel_details; + if (!getChannel(channel_idx, channel_details)) { + strcpy(channel_details.name, "Unknown"); + } + _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onQueueSizeChanged(offline_queue_len); } - if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len); -#endif } void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, @@ -930,9 +920,9 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} -MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) { _iter_started = false; _cli_rescue = false; cli_command[0] = 0; @@ -1463,9 +1453,7 @@ void MyMesh::handleCmdFrame(size_t len) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); -#endif + if (_listener) _listener->onQueueSizeChanged(offline_queue_len); } else { out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; _serial->writeFrame(out_frame, 1); @@ -2466,10 +2454,6 @@ void MyMesh::loop() { saveContacts(); dirty_contacts_expiry = 0; } - -#ifdef DISPLAY_CLASS - if (_ui) _ui->setHasConnection(_serial->isConnected()); -#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3b98a4f6..be7bc40a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -2,7 +2,6 @@ #include #include -#include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ #define FIRMWARE_VER_CODE 14 @@ -96,10 +95,19 @@ struct DiscoveredNode { class MyMesh : public BaseChatMesh, public DataStoreHost { public: - MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); + class Listener { + public: + virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onQueueSizeChanged(int msgcount) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + }; + + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); void begin(bool has_display); void startInterface(BaseSerialInterface &serial); + void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); @@ -237,7 +245,7 @@ private: uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; - AbstractUITask* _ui; + Listener* _listener; ContactsIterator _iter; uint32_t _iter_filter_since; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc..857acf45 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -114,11 +114,7 @@ MultiSerialInterface interface_manager; StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif -); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); /* END GLOBAL OBJECTS */ @@ -285,6 +281,7 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved + the_mesh.setListener(&ui_task); #endif board.onBootComplete(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 53ae480c..fc6707e2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,17 +751,7 @@ switch(t){ #endif } - -void UITask::msgRead(int msgcount) { - _msgcount = msgcount; - if (msgcount == 0) { - gotoHomeScreen(); - } -} - -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); @@ -774,6 +764,42 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); + setCurrScreen(msg_preview); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onQueueSizeChanged(int msgcount) { + _msgcount = msgcount; + if (msgcount == 0) { + gotoHomeScreen(); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52d3ffa1..e1200953 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -90,10 +90,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b49b05dd..08c84587 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -126,7 +126,7 @@ switch(t){ // Serial.println((int) t); } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { clearMsgPreview(); @@ -139,9 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -158,10 +156,45 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _display->turnOn(); } if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _need_refresh = true; + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +#ifdef HAS_DRV2605 + vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) +#endif + + if (path_len == 0xFF) { + sprintf(_origin, "(F) %s", channel_details.name); + } else { + sprintf(_origin, "(%d) %s", (uint32_t) path_len, channel_details.name); + } + StrHelper::strncpy(_msg, text, sizeof(_msg)); + + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _need_refresh = true; + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { @@ -262,7 +295,7 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && the_mesh.getBLEPin() != 0) { + if (!hasConnection() && the_mesh.getBLEPin() != 0) { _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 02d126e8..38141479 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -70,9 +70,13 @@ public: bool hasDisplay() const { return _display != NULL; } void clearMsgPreview(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 9ecd66f1..8a8b77b5 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -503,16 +503,14 @@ switch(t){ } -void UITask::msgRead(int msgcount) { +void UITask::onQueueSizeChanged(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { gotoHomeScreen(); } } -void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { - _msgcount = msgcount; - +void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -522,6 +520,30 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _next_refresh = 100; // trigger refresh } } + if (!hasConnection()) { + notify(UIEventType::contactMessage); + } +} + +void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { + if (_display != NULL) { + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer + _next_refresh = 100; // trigger refresh + } + } + if (!hasConnection()) { + notify(UIEventType::channelMessage); + } +} + +void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (!hasConnection()) { + notify(UIEventType::newContactMessage); + } } void UITask::userLedHandler() { diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index dc689478..925e8d71 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -98,10 +98,13 @@ public: bool getGPSState(); void toggleGPS(); + // MyMesh::Listener + void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onQueueSizeChanged(int offline_queue_size) override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; // from AbstractUITask - void msgRead(int msgcount) override; - void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void notify(UIEventType t = UIEventType::none) override; void loop() override; From b71933beb5d618aa74f99c4916bda659bb6e22a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Wed, 16 Sep 2026 22:28:55 +0200 Subject: [PATCH 124/174] Enable DC/DC in shared LR1110 initialization --- src/helpers/radiolib/CustomLR1110.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1d..f0fde435 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1110::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { From 5c3d9281ffbd5c1cb8ee5570c0d89aebbac91521 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 13:45:33 +1000 Subject: [PATCH 125/174] * refactored the UI_DISCOVER_* feature --- examples/companion_radio/MyMesh.cpp | 64 ++---------------- examples/companion_radio/MyMesh.h | 35 ++-------- examples/companion_radio/ui-new/UITask.cpp | 72 +++++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 23 ++++++- examples/companion_radio/ui-orig/UITask.cpp | 12 +++- examples/companion_radio/ui-orig/UITask.h | 3 +- examples/companion_radio/ui-tiny/UITask.cpp | 8 ++- examples/companion_radio/ui-tiny/UITask.h | 3 +- 8 files changed, 112 insertions(+), 108 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7becb0df..6e570ea5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -139,10 +139,6 @@ #define ERR_CODE_FILE_IO_ERROR 5 #define ERR_CODE_ILLEGAL_ARG 6 -// Copied from simple_repeater (could probably be shared) -#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 -#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 - #define MAX_SIGN_DATA_LEN (8 * 1024) // 8K // Auto-add config bitmask @@ -410,53 +406,6 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) { - if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE; - if (max_num > disc_nodes_count) max_num = disc_nodes_count; - - for (int i = 0; i < max_num; i++) { - nodes[i] = discovered_nodes[i]; - } - return max_num; -} - -bool MyMesh::requestRepeatersDiscovery() { - uint8_t cmd_bytes[6]; - cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only - cmd_bytes[1] = 0xFF; // Repeaters - getRNG()->random(&cmd_bytes[2], 4); // tag - disc_nodes_count = 0; - disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]); - mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes)); - if (req) { - sendZeroHop(req); - return true; - } - return false; -} - -void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) { - if ((p_len < 12) - || (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP) - || (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE) - || (memcmp(&payload[2], &disc_node_req_tag, 4))) { - return; - } - memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8); - discovered_nodes[disc_nodes_count].type = payload[0] & 0xF; - discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0; - discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR(); - ContactInfo* c = lookupContactByPubKey(&payload[6], 8); - if (c != NULL) { - strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32); - } else { - discovered_nodes[disc_nodes_count].name[0] = 0; - } - disc_nodes_count ++; -} -#endif - void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -518,10 +467,8 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _listener) { - _listener->onMessageRecv(path_len, from.name, text); + if (_listener) { + _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -840,9 +787,6 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); return; } -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - checkControlDataForPendingDiscovery(packet->payload, packet->payload_len); -#endif int i = 0; out_frame[i++] = PUSH_CODE_CONTROL_DATA; out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); @@ -856,6 +800,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); } + + if (_listener) _listener->onControlDataRecv(packet); } void MyMesh::onRawDataRecv(mesh::Packet *packet) { @@ -1039,7 +985,7 @@ void MyMesh::begin(bool has_display) { resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure public channel _store->loadChannels(this); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index be7bc40a..f1128624 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -75,6 +75,10 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +// Copied from simple_repeater +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + struct AdvertPath { uint8_t pubkey_prefix[7]; uint8_t path_len; @@ -83,24 +87,15 @@ struct AdvertPath { uint8_t path[MAX_PATH_SIZE]; }; -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) -struct DiscoveredNode { - uint8_t pubkey_prefix[9]; - float snr_in; - float snr_out; - char name[32]; - uint8_t type; -}; -#endif - class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) = 0; + virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + virtual void onControlDataRecv(const mesh::Packet* packet) = 0; }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); @@ -120,11 +115,6 @@ public: int getRecentlyHeard(AdvertPath dest[], int max_num); -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - bool requestRepeatersDiscovery(); - int getDiscoveredNodes(DiscoveredNode nodes[], int max_num); -#endif - protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -287,19 +277,6 @@ private: #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table - -#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - #ifdef UI_RECENT_LIST_SIZE - #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE - #else - #define DISCOVERED_NODES_TABLE_SIZE 4 - #endif - DiscoveredNode discovered_nodes[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept - uint32_t disc_node_req_tag = 0; - uint32_t disc_nodes_count = 0; - - void checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len); -#endif }; extern MyMesh the_mesh; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index fc6707e2..33a4e192 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -99,7 +99,7 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN DISCOVERY, #endif #ifndef UI_NO_HIBERNATE @@ -115,8 +115,10 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) - DiscoveredNode discovered[UI_RECENT_LIST_SIZE]; +#if UI_DISCOVER_SCREEN + DiscoveredNode discovered[DISCOVERED_NODES_TABLE_SIZE]; // not circular, latest discovered nodes are not kept + uint32_t disc_node_req_tag = 0; + uint32_t disc_nodes_count = 0; uint32_t discovery_req_time = 0; bool discovery_disp_names = true; // by default desplay names if available (removes SNR_O) #endif @@ -206,6 +208,40 @@ public: : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false), sensors_lpp(200) { } +#if UI_DISCOVER_SCREEN + bool sendDiscoverRequest() { + uint8_t cmd_bytes[6]; + cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only + cmd_bytes[1] = 0xFF; // Repeaters + the_mesh.getRNG()->random((uint8_t *) &disc_node_req_tag, 4); // generate random tag + memcpy(&cmd_bytes[2], &disc_node_req_tag, 4); + disc_nodes_count = 0; + mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes)); + if (req) { + the_mesh.sendZeroHop(req); + discovery_req_time = millis(); + return true; + } + return false; + } + + void handleDiscoverResponse(const mesh::Packet* packet) { + if (disc_nodes_count < DISCOVERED_NODES_TABLE_SIZE && memcmp(&packet->payload[2], &disc_node_req_tag, 4) == 0) { + auto d = &discovered[disc_nodes_count++]; + memcpy(d->pubkey_prefix, &packet->payload[6], 8); + d->type = packet->payload[0] & 0xF; + d->snr_out = ((int8_t)packet->payload[1]) / 4.0; + d->snr_in = radio_driver.getLastSNR(); + ContactInfo* c = the_mesh.lookupContactByPubKey(&packet->payload[6], 8); + if (c != NULL) { + strncpy(d->name, c->name, 32); + } else { + d->name[0] = 0; + } + } + } +#endif + void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); @@ -464,9 +500,9 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN } else if (_page == HomePage::DISCOVERY) { - int count = the_mesh.getDiscoveredNodes(discovered, UI_RECENT_LIST_SIZE); + int count = disc_nodes_count; display.setColor(UIColor::primary_txt); int y = 20; for (int i = 0; i < count; i++, y += 11) { @@ -493,8 +529,8 @@ public: } if (millis() < discovery_req_time + 5000) { return 1000; // more frequent updates just after req - } else if (count < UI_RECENT_LIST_SIZE -1) { // show only 5 sec after last disc - y = 10 + 11 * UI_RECENT_LIST_SIZE; + } else if (count < DISCOVERED_NODES_TABLE_SIZE -1) { // show only 5 sec after last disc + y = 10 + 11 * DISCOVERED_NODES_TABLE_SIZE; display.drawTextCentered(display.width() / 2, y, "discover: " PRESS_LABEL); } #endif @@ -525,7 +561,7 @@ public: if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (_page == HomePage::DISCOVERY) { _task->showAlert("Repeater disc", 800); } @@ -562,11 +598,10 @@ public: return true; } #endif -#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0)) +#if UI_DISCOVER_SCREEN if (c == KEY_ENTER && _page == HomePage::DISCOVERY) { if (millis() > discovery_req_time + 5000) { // rate limiter - the_mesh.requestRepeatersDiscovery(); - discovery_req_time = millis(); + sendDiscoverRequest(); } return true; } @@ -751,8 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { - ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + + ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); if (_display != NULL) { @@ -802,6 +840,14 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +#if UI_DISCOVER_SCREEN + if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + ((HomeScreen *) home)->handleDiscoverResponse(packet); + } +#endif +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index e1200953..842b1ac9 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,26 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifndef UI_DISCOVER_SCREEN + #define UI_DISCOVER_SCREEN 1 +#endif + +#if UI_DISCOVER_SCREEN + #ifdef UI_RECENT_LIST_SIZE + #define DISCOVERED_NODES_TABLE_SIZE UI_RECENT_LIST_SIZE + #else + #define DISCOVERED_NODES_TABLE_SIZE 4 + #endif + +struct DiscoveredNode { + float snr_in; + float snr_out; + uint8_t pubkey_prefix[9]; + uint8_t type; + char name[32]; +}; +#endif + class UITask : public AbstractUITask { DisplayDriver* _display; SensorManager* _sensors; @@ -91,10 +111,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 08c84587..45a81da9 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,15 +139,18 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif if (path_len == 0xFF) { - sprintf(_origin, "(F) %s", from_name); + sprintf(_origin, "(F) %s", from.name); } else { - sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name); + sprintf(_origin, "(%d) %s", (uint32_t) path_len, from.name); } StrHelper::strncpy(_msg, text, sizeof(_msg)); @@ -197,6 +200,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 38141479..553dc7d0 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,10 +71,11 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 8a8b77b5..71a8bb21 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,10 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(uint8_t path_len, const char* from_name, const char* text) { +void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -546,6 +549,9 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } +void UITask::onControlDataRecv(const mesh::Packet* packet) { +} + void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 925e8d71..48c697b9 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,10 +99,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(uint8_t path_len, const char* from_name, const char* text) override; + void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From cd54ee9754a0f77d31a69bb99088aec78cd5ee7c Mon Sep 17 00:00:00 2001 From: liamcottle Date: Thu, 17 Sep 2026 15:52:46 +1200 Subject: [PATCH 126/174] add missing null display driver class for thinknode m7 ethernet variant --- variants/thinknode_m7/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 5606d482..69b38e33 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -169,6 +169,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} From 30dd723c26cee71fc43dc54abb56cbf17d431a4e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 14:25:41 +1000 Subject: [PATCH 127/174] * MyMesh::begin() refactor, removing last display concepts --- examples/companion_radio/MyMesh.cpp | 24 ++++------------ examples/companion_radio/MyMesh.h | 3 +- examples/companion_radio/main.cpp | 43 +++++++++++++++-------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6e570ea5..409fed9f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -911,7 +911,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe #endif } -void MyMesh::begin(bool has_display) { +void MyMesh::begin() { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { @@ -963,24 +963,7 @@ void MyMesh::begin(bool has_display) { _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours -#ifdef BLE_PIN_CODE // 123456 by default - if (_prefs.ble_pin == 0) { -#ifdef DISPLAY_CLASS - if (has_display && BLE_PIN_CODE == 123456) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } -#else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin -#endif - } else { - _active_ble_pin = _prefs.ble_pin; - } -#else - _active_ble_pin = 0; -#endif + _active_ble_pin = _prefs.ble_pin; resetContacts(); _store->loadContacts(this); @@ -1007,6 +990,9 @@ NodePrefs *MyMesh::getNodePrefs() { uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +void MyMesh::setBLEPin(uint32_t active_pin) { + _active_ble_pin = active_pin; +} struct FreqRange { uint32_t lower_freq, upper_freq; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f1128624..7bfbb292 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -100,13 +100,14 @@ public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); - void begin(bool has_display); + void begin(); void startInterface(BaseSerialInterface &serial); void setListener(Listener* listener) { _listener = listener; } const char *getNodeName(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); + void setBLEPin(uint32_t active_pin); void loop(); void handleCmdFrame(size_t len); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 857acf45..efc31779 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -172,37 +172,38 @@ void setup() { #endif #endif store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); - the_mesh.begin( - #ifdef DISPLAY_CLASS - disp != NULL - #else - false - #endif - ); + the_mesh.begin(); #else #error "need to define filesystem" #endif +#ifdef BLE_PIN_CODE // 123456 by default + if (the_mesh.getNodePrefs()->ble_pin == 0) { +#ifdef DISPLAY_CLASS + if (disp != NULL && BLE_PIN_CODE == 123456) { + StdRNG rng; + the_mesh.setBLEPin(rng.nextInt(100000, 999999)); // random pin each session + } else { + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin + } +#else + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin +#endif + } else { + the_mesh.setBLEPin(the_mesh.getNodePrefs()->ble_pin); + } +#else + the_mesh.setBLEPin(0); +#endif + // add bluetooth interface #if defined(BLE_PIN_CODE) bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); From b3b17025e38c3c486fa414f7bf31d26f8686ec5b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 17 Sep 2026 17:33:36 +1000 Subject: [PATCH 128/174] * more MyMesh::Listener interface hooks --- examples/companion_radio/MyMesh.cpp | 20 +++++++++++++++++++- examples/companion_radio/MyMesh.h | 15 ++++++++++++--- examples/companion_radio/ui-new/UITask.cpp | 2 +- examples/companion_radio/ui-new/UITask.h | 2 +- examples/companion_radio/ui-orig/UITask.cpp | 5 +---- examples/companion_radio/ui-orig/UITask.h | 3 +-- examples/companion_radio/ui-tiny/UITask.cpp | 5 +---- examples/companion_radio/ui-tiny/UITask.h | 3 +-- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 409fed9f..61288a6e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -415,6 +415,11 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { } ContactInfo* MyMesh::processAck(const uint8_t *data) { + if (_listener) { + uint32_t ack_crc; + memcpy(&ack_crc, data, 4); + _listener->onACKRecv(ack_crc); + } // see if matches any in a table for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient @@ -588,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMsgRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(channel_details, path_len, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -626,6 +631,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } + if (_listener) { + _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + } } uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, @@ -673,6 +681,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim memcpy(&reply[4], telemetry.getBuffer(), tlen); return 4 + tlen; } + } else if (_listener) { + return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply); } return 0; // unknown } @@ -748,6 +758,8 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (_listener && len > 4) { + _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); } } @@ -822,6 +834,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); } + if (_listener) { + _listener->onRawDataRecv(packet); + } } void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, @@ -852,6 +867,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } + if (_listener) { + _listener->onTraceRecv(packet, tag, auth_code, flags, path_snrs, path_hashes, path_len); + } } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 7bfbb292..667b19e1 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -92,10 +92,19 @@ public: class Listener { public: virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; - virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; - virtual void onControlDataRecv(const mesh::Packet* packet) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } + virtual void onControlDataRecv(const mesh::Packet* packet) { } + virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + const uint8_t *data, size_t data_len) { } + virtual void onACKRecv(uint32_t ack_crc) { } + virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } + virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } + virtual void onRawDataRecv(mesh::Packet *packet) { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 33a4e192..8457367a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -808,7 +808,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 842b1ac9..47928a17 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -112,7 +112,7 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; void onControlDataRecv(const mesh::Packet* packet) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 45a81da9..42e4ab79 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -168,7 +168,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif @@ -200,9 +200,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 553dc7d0..bb879cb6 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -72,10 +72,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 71a8bb21..607ac89c 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); @@ -549,9 +549,6 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } -void UITask::onControlDataRecv(const mesh::Packet* packet) { -} - void UITask::userLedHandler() { #ifdef PIN_STATUS_LED int cur_time = millis(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 48c697b9..83591d89 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -100,10 +100,9 @@ public: // MyMesh::Listener void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMsgRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; From 1077331e3162e8e80c76b900a0c8275d4fed2465 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Tue, 8 Sep 2026 16:57:12 -0400 Subject: [PATCH 129/174] Extend WiFi disconnect logging to specify reason Today if a user builds firmware with an incorrect WiFi SSID or password, they get a generic `WiFi disconnected.` error message. Extend logging to descirbe the reason that WiFi was disconnected. I gathered the updated logs for the 3 scenarios below. ``` WiFi: Attempting manual WiFi reconnect... WiFi: WiFi disconnected (reason=ASSOC_LEAVE). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... WiFi: WiFi disconnected (reason=4WAY_HANDSHAKE_TIMEOUT). Flagging for reconnect... ``` ``` WiFi: WiFi disconnected (reason=NO_AP_FOUND). Flagging for reconnect... ``` ``` WiFi: WiFi connected successfully! WiFi: Got connection ``` --- examples/companion_radio/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dc9acdc..0e989999 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -229,7 +229,8 @@ void setup() { WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); + WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...", + WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason)); wifi_needs_reconnect = true; } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); From 8d2a5657accd311e3f75578c46882313bdd4ec65 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Thu, 17 Sep 2026 22:12:53 -0400 Subject: [PATCH 130/174] Pin Radiolab for stm32 and replace ltoa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The major rev from 19.x to 20.x breaks Radiolab, so pin it for now. This should fix the failing wio-e5-mini_repeater builds. ltoa is removed upstream, so it was replaced with snprintf. The p[] buffer is always 16 bytes. Copy/paste for why claude thinks this is fine below, but also if this is broken it wouldn't be new. ``` exp2 in [23, 30] (int_part = mantissa << (exp2 - 23)): this is where int_part can reach its true max, up to 2,147,483,520 — 10 digits. That's what the snprintf bound of 11 (10 digits + NUL) is sized for. But in this branch frac_part is never assigned anything other than its initial 0, so only *p++ = '0' runs for the fraction — 1 char, not 7. exp2 in [0, 22] (int_part = mantissa >> (23 - exp2)): this is the only branch where frac_part can also be nonzero and enter the 7-digit loop. Here int_part < 2^23 by construction, so it's capped at 7 digits, never 10. ``` --- platformio.ini | 4 +++- src/helpers/TxtDataHelpers.cpp | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index de4d6c29..f296f838 100644 --- a/platformio.ini +++ b/platformio.ini @@ -110,7 +110,9 @@ build_flags = ${arduino_base.build_flags} [stm32_base] extends = arduino_base -platform = ststm32 +; Pinned pending upstream fix for upstream conversions. +; https://github.com/jgromes/RadioLib/issues/1873 +platform = ststm32@19.7.1 extra_scripts = post:arch/stm32/build_hex.py build_flags = ${arduino_base.build_flags} -D STM32_PLATFORM diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index d327931f..f2621c0d 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -27,8 +27,9 @@ bool StrHelper::isBlank(const char* str) { } #include +#include -union int32_Float_t +union int32_Float_t { int32_t Long; float Float; @@ -100,11 +101,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '-'; if (int_part == 0) *p++ = '0'; - else + else { - ltoa(int_part, p, 10); - while (*p) - p++; + p += snprintf(p, 11, "%ld", (long)int_part); } *p++ = '.'; if (frac_part == 0) From 242e401a2c9f5b88980a170fba63b42a571081d3 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 18 Sep 2026 18:17:52 +1200 Subject: [PATCH 131/174] added reboot, shutdown and poweroff to companion cli commands --- examples/companion_radio/MyMesh.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 01a7473a..c38fa70a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2146,6 +2146,16 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* return true; } + if (strcmp(command, "reboot") == 0) { + board.reboot(); // doesn't return + return true; + } + + if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) { + board.powerOff(); // doesn't return + return true; + } + if (memcmp(command, "set name ", 9) == 0) { if (AdvertDataParser::isValidName(&command[9])) { StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name)); From 843598724c34ee6857dec63d58383f5d49156303 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 18 Sep 2026 19:03:20 +1000 Subject: [PATCH 132/174] * fix for X1 --- examples/companion_radio/ui-orig/UITask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 42e4ab79..a717a540 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -408,7 +408,7 @@ void UITask::userLedHandler() { statusLedWrite(255, 0, 0); // red: battery low } else if (_msgcount > 0) { statusLedWrite(255, 90, 0); // amber: unread messages - } else if (_connected) { + } else if (hasConnection()) { statusLedWrite(0, 0, 255); // blue: app connected } else { statusLedWrite(0, 255, 0); // green: heartbeat From 448d1b2239859ced5e44047bdc89717122ac626e Mon Sep 17 00:00:00 2001 From: Alexander Wunschik Date: Fri, 18 Sep 2026 16:59:26 +0200 Subject: [PATCH 133/174] docs: fix frame format tables --- docs/kiss_modem_protocol.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 3f4dbf9c..7ff48786 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -18,10 +18,10 @@ Standard KISS framing per the KA9Q/K3MC specification. | `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) | ``` -┌──────┬───────────┬──────────────┬──────┐ -│ FEND │ Type Byte │ Data (escaped)│ FEND │ -│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ -└──────┴───────────┴──────────────┴──────┘ +┌──────┬───────────┬────────────────┬──────┐ +│ FEND │ Type Byte │ Data (escaped) │ FEND │ +│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ +└──────┴───────────┴────────────────┴──────┘ ``` ### Type Byte @@ -82,10 +82,10 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The ### Frame Format ``` -┌──────┬──────┬─────────────┬──────────────┬──────┐ -│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │ -│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │ -└──────┴──────┴─────────────┴──────────────┴──────┘ +┌──────┬──────┬─────────────┬────────────────┬──────┐ +│ FEND │ 0x06 │ Sub-command │ Data (escaped) │ FEND │ +│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │ +└──────┴──────┴─────────────┴────────────────┴──────┘ ``` ### Request Sub-commands (Host to TNC) From c425648bfc56db6103536426dc155274093c3898 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 19 Sep 2026 11:26:42 +0700 Subject: [PATCH 134/174] Off LED before powering off for repeater --- examples/simple_repeater/UITask.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557..e0b6a41d 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -140,8 +140,10 @@ void UITask::loop() { } if (_powering_off_at > 0) { // power off timer armed -#ifdef LED_PIN +#if defined(LED_PIN) && defined(LED_STATE_ON) digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff + delay(1000); + digitalWrite(LED_PIN, !LED_STATE_ON); // off LED #endif if (millis() > _powering_off_at) { _board->powerOff(); // should not return From 64434c5326b080283a5576ef1601addc5bae52f4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 19 Sep 2026 20:13:18 +1000 Subject: [PATCH 135/174] * misc --- examples/companion_radio/MyMesh.cpp | 6 +++--- examples/companion_radio/MyMesh.h | 13 +++++++------ examples/companion_radio/ui-new/UITask.cpp | 8 +++++--- examples/companion_radio/ui-new/UITask.h | 10 +++++++--- examples/companion_radio/ui-orig/UITask.cpp | 6 ++++-- examples/companion_radio/ui-orig/UITask.h | 4 ++-- examples/companion_radio/ui-tiny/UITask.cpp | 4 ++-- examples/companion_radio/ui-tiny/UITask.h | 4 ++-- 8 files changed, 32 insertions(+), 23 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 61288a6e..e1b031a9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -473,7 +473,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe } if (_listener) { - _listener->onMessageRecv(from, txt_type, sender_timestamp, path_len, text); + _listener->onMessageRecv(pkt, from, txt_type, sender_timestamp, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -593,7 +593,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (!getChannel(channel_idx, channel_details)) { strcpy(channel_details.name, "Unknown"); } - _listener->onChannelMessageRecv(channel_details, path_len, text); + _listener->onChannelMessageRecv(pkt, channel_details, text); _listener->onQueueSizeChanged(offline_queue_len); } } @@ -632,7 +632,7 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * _serial->writeFrame(frame, 1); } if (_listener) { - _listener->onChannelDataRecv(channel, pkt, data_type, data, data_len); + _listener->onChannelDataRecv(pkt, channel, data_type, data, data_len); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 667b19e1..9f077c30 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -91,20 +91,21 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: class Listener { public: - virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0; - virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0; + virtual void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) = 0; + virtual void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) = 0; virtual void onQueueSizeChanged(int msgcount) = 0; virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } - virtual void onControlDataRecv(const mesh::Packet* packet) { } - virtual void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, + virtual void onControlDataRecv(const mesh::Packet* pkt) { } + virtual void onChannelDataRecv(mesh::Packet *pkt, const mesh::GroupChannel &channel, uint16_t data_type, const uint8_t *data, size_t data_len) { } virtual void onACKRecv(uint32_t ack_crc) { } virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } - virtual void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } - virtual void onRawDataRecv(mesh::Packet *packet) { } + virtual void onRawDataRecv(mesh::Packet *pkt) { } + virtual ~Listener() { } }; MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8457367a..181bc832 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -786,10 +786,11 @@ switch(t){ #endif } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from.name, text); setCurrScreen(msg_preview); @@ -808,7 +809,8 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, channel_details.name, text); setCurrScreen(msg_preview); @@ -842,7 +844,7 @@ void UITask::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path void UITask::onControlDataRecv(const mesh::Packet* packet) { #if UI_DISCOVER_SCREEN - if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { + if (packet->payload_len >= 14 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) { ((HomeScreen *) home)->handleDiscoverResponse(packet); } #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 47928a17..361b04d0 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -22,6 +22,10 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" +#ifdef UI_NO_DISCOVER_SCREEN + #error "UI_NO_DISCOVER_SCREEN is obsolete - use -D UI_DISCOVER_SCREEN=0 instead" +#endif + #ifndef UI_DISCOVER_SCREEN #define UI_DISCOVER_SCREEN 1 #endif @@ -111,11 +115,11 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; - void onControlDataRecv(const mesh::Packet* packet) override; + void onControlDataRecv(const mesh::Packet* pkt) override; // from AbstractUITask void notify(UIEventType t = UIEventType::none) override; diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index a717a540..db8772f4 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -139,7 +139,7 @@ void UITask::clearMsgPreview() { _need_refresh = true; } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -147,6 +147,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", from.name); } else { @@ -168,11 +169,12 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { #ifdef HAS_DRV2605 vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown) #endif + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; if (path_len == 0xFF) { sprintf(_origin, "(F) %s", channel_details.name); } else { diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index bb879cb6..d5f79898 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -71,8 +71,8 @@ public: void clearMsgPreview(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 607ac89c..7b70f836 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -510,7 +510,7 @@ void UITask::onQueueSizeChanged(int msgcount) { } } -void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) { +void UITask::onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; @@ -528,7 +528,7 @@ void UITask::onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t s } } -void UITask::onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) { +void UITask::onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) { if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 83591d89..084919aa 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -99,8 +99,8 @@ public: void toggleGPS(); // MyMesh::Listener - void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) override; - void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) override; + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override; + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override; void onQueueSizeChanged(int offline_queue_size) override; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; From 73afe3a0f548fc1df65c2afff4ae9eea1a1def54 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 14:22:13 +1200 Subject: [PATCH 136/174] allow fetching configured wifi password via companion cli --- examples/companion_radio/MyMesh.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c38fa70a..79b4a8ce 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2191,6 +2191,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi.pwd updated (reboot to apply)"); return true; } + if (strcmp(command, "get wifi.pwd") == 0) { + sprintf(reply, "> %s", _prefs.wifi_pwd); + return true; + } if (strcmp(command, "set wifi.clear") == 0) { _prefs.wifi_ssid[0] = 0; _prefs.wifi_pwd[0] = 0; @@ -2198,7 +2202,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* strcpy(reply, "> wifi config cleared (reboot to apply)"); return true; } - if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design + if (strcmp(command, "get wifi.ssid") == 0) { sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); return true; } From 4021a341f8bed9250f3bd145de9412b179f1dcdb Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 20 Sep 2026 15:11:21 +1200 Subject: [PATCH 137/174] add wifi support to thinknode m7 ble variant --- variants/thinknode_m7/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 69b38e33..128b1244 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -98,11 +98,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> From 798ce636df392572fea3186f5382b44fc4d1c486 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:47:24 -0700 Subject: [PATCH 138/174] Address review feedback on Muzi Base support - rename the board class and files to MuziBaseBoard - uppercase the build flags (MUZI_BASE, MUZI_BASE_SUPERIO) - name the superIO envs muzi_base_{duo,uno}_superIO_companion_radio_ble so build.sh picks them up - set MAX_CONTACTS to 350 like the other variants - move the pin setup from initVariant() into MuziBaseBoard::begin() --- .../{muzi_baseBoard.cpp => MuziBaseBoard.cpp} | 20 ++++++++++--- .../{muzi_baseBoard.h => MuziBaseBoard.h} | 4 +-- variants/muzi_base/platformio.ini | 22 +++++++-------- variants/muzi_base/target.cpp | 2 +- variants/muzi_base/target.h | 6 ++-- variants/muzi_base/variant.cpp | 28 ------------------- 6 files changed, 33 insertions(+), 49 deletions(-) rename variants/muzi_base/{muzi_baseBoard.cpp => MuziBaseBoard.cpp} (69%) rename variants/muzi_base/{muzi_baseBoard.h => MuziBaseBoard.h} (91%) diff --git a/variants/muzi_base/muzi_baseBoard.cpp b/variants/muzi_base/MuziBaseBoard.cpp similarity index 69% rename from variants/muzi_base/muzi_baseBoard.cpp rename to variants/muzi_base/MuziBaseBoard.cpp index ed713fa0..a104c550 100644 --- a/variants/muzi_base/muzi_baseBoard.cpp +++ b/variants/muzi_base/MuziBaseBoard.cpp @@ -1,7 +1,7 @@ #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { @@ -10,7 +10,7 @@ const PowerMgtConfig power_config = { .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK }; -void muzi_baseBoard::initiateShutdown(uint8_t reason) { +void MuziBaseBoard::initiateShutdown(uint8_t reason) { // Disable LoRa module power before shutdown if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { @@ -21,14 +21,26 @@ void muzi_baseBoard::initiateShutdown(uint8_t reason) { } #endif // NRF52_POWER_MANAGEMENT -void muzi_baseBoard::begin() { +void MuziBaseBoard::begin() { NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); -#ifdef muzi_base_superIO + pinMode(PIN_BATTERY_CHARGING, INPUT); + pinMode(PIN_CHARGER_FAULT, INPUT); + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + // output latches default to HIGH, so pull these low right after enabling + pinMode(BUZZER_PIN, OUTPUT); + digitalWrite(BUZZER_PIN, LOW); + // gps power is driven by the sensor manager (mode switch). off to start. + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); // 12V rail is only needed for the superIO display pinMode(SCREEN_12V_ENABLE, OUTPUT); +#ifdef MUZI_BASE_SUPERIO digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display delay(250); +#else + digitalWrite(SCREEN_12V_ENABLE, LOW); #endif Wire.begin(); // delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot. diff --git a/variants/muzi_base/muzi_baseBoard.h b/variants/muzi_base/MuziBaseBoard.h similarity index 91% rename from variants/muzi_base/muzi_baseBoard.h rename to variants/muzi_base/MuziBaseBoard.h index e5412b6a..a5a4abbe 100644 --- a/variants/muzi_base/muzi_baseBoard.h +++ b/variants/muzi_base/MuziBaseBoard.h @@ -15,14 +15,14 @@ #define MUZI_BASE_MFR_NAME "Muzi Base Duo" #endif -class muzi_baseBoard : public NRF52BoardDCDC { +class MuziBaseBoard : public NRF52BoardDCDC { protected: #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif public: - muzi_baseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} + MuziBaseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {} void begin(); #define BATTERY_SAMPLES 8 diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index eb35d569..46a0ae4d 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -17,7 +17,7 @@ build_flags = ${nrf52_base.build_flags} -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 -I variants/muzi_base -I src/helpers/ui - -D muzi_base + -D MUZI_BASE -D NRF52_POWER_MANAGEMENT -D PIN_USER_BTN=PIN_BUTTON1 -D USER_BTN_PRESSED=LOW @@ -106,7 +106,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -125,7 +125,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -150,7 +150,7 @@ extends = muzi_base_duo board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -174,12 +174,12 @@ lib_deps = ${muzi_base_duo.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_duo_companion_radio_ble_superIO] +[env:muzi_base_duo_superIO_companion_radio_ble] extends = muzi_base_duo_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_duo_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 @@ -235,7 +235,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -254,7 +254,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=500 ;can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D BLE_TX_POWER=0 @@ -279,7 +279,7 @@ extends = muzi_base_uno board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno.build_flags} - -D muzi_base_superIO + -D MUZI_BASE_SUPERIO -D UI_HAS_JOYSTICK=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1107Display @@ -303,12 +303,12 @@ lib_deps = ${muzi_base_uno.lib_deps} debug_tool = jlink upload_protocol = nrfutil -[env:muzi_base_uno_companion_radio_ble_superIO] +[env:muzi_base_uno_superIO_companion_radio_ble] extends = muzi_base_uno_superIO board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${muzi_base_uno_superIO.build_flags} - -D MAX_CONTACTS=500 ; can increase number of contacts since we have a ton of extra flash. + -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 diff --git a/variants/muzi_base/target.cpp b/variants/muzi_base/target.cpp index 2af0c710..5bd846c9 100644 --- a/variants/muzi_base/target.cpp +++ b/variants/muzi_base/target.cpp @@ -3,7 +3,7 @@ #include "target.h" #include "variant.h" -muzi_baseBoard board; +MuziBaseBoard board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); diff --git a/variants/muzi_base/target.h b/variants/muzi_base/target.h index cbcfff14..45cd595f 100644 --- a/variants/muzi_base/target.h +++ b/variants/muzi_base/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include "muzi_baseBoard.h" +#include "MuziBaseBoard.h" #if defined(USE_LR1121) #include #elif defined(USE_SX1262) @@ -19,7 +19,7 @@ #include -#ifdef muzi_base_superIO +#ifdef MUZI_BASE_SUPERIO #include extern DISPLAY_CLASS display; extern MomentaryButton user_btn; @@ -32,7 +32,7 @@ extern MomentaryButton user_btn; #endif -extern muzi_baseBoard board; +extern MuziBaseBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; diff --git a/variants/muzi_base/variant.cpp b/variants/muzi_base/variant.cpp index b179af8a..cbde3355 100644 --- a/variants/muzi_base/variant.cpp +++ b/variants/muzi_base/variant.cpp @@ -60,31 +60,3 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 46, 47, }; - -void initVariant() -{ - // All pins output HIGH by default. - // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69 - - pinMode(PIN_VBAT_READ, INPUT); - pinMode(PIN_BATTERY_CHARGING, INPUT); - pinMode(PIN_CHARGER_FAULT, INPUT); - pinMode(PIN_BUTTON1, INPUT); - pinMode(PIN_BUTTON2, INPUT); - pinMode(PIN_BUTTON3, INPUT); - pinMode(PIN_BUTTON4, INPUT); - pinMode(PIN_BUTTON5, INPUT); - pinMode(PIN_BUTTON6, INPUT); - pinMode(LED_PIN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_PIN, LOW); - digitalWrite(LED_BLUE, LOW); - pinMode(BUZZER_PIN, OUTPUT); - digitalWrite(BUZZER_PIN, LOW); // turn off buzzer at start. don't leave it high. - // gps power is driven by the sensor manager (mode switch). off to start. - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, LOW); - - pinMode(SCREEN_12V_ENABLE, OUTPUT); - digitalWrite(SCREEN_12V_ENABLE, LOW); // disable 12V power for SH1107 display for now. -} From 114cf9f4fcd0a1486ce01fa4bbf6dd6575a217f3 Mon Sep 17 00:00:00 2001 From: TJ Downes <273720+tjdownes@users.noreply.github.com> Date: Sun, 20 Sep 2026 03:46:25 -0700 Subject: [PATCH 139/174] Use the default BLE TX power on Muzi Base Drop BLE_TX_POWER=0 from the companion envs so they use the default of 4 dBm, like most other variants. The lower power was carried over from the original Base Duo PR and real-world range is better at the default. --- variants/muzi_base/platformio.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/variants/muzi_base/platformio.ini b/variants/muzi_base/platformio.ini index 46a0ae4d..72a3ecac 100644 --- a/variants/muzi_base/platformio.ini +++ b/variants/muzi_base/platformio.ini @@ -128,7 +128,6 @@ build_flags = ${muzi_base_duo.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -183,7 +182,6 @@ build_flags = ${muzi_base_duo_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -257,7 +255,6 @@ build_flags = ${muzi_base_uno.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 @@ -312,7 +309,6 @@ build_flags = ${muzi_base_uno_superIO.build_flags} -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D BLE_PIN_CODE=123456 - -D BLE_TX_POWER=0 -D QSPIFLASH=1 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 From 12a8c8d4d42bf42189526a02906d328a893f2177 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Sun, 20 Sep 2026 15:16:33 -0400 Subject: [PATCH 140/174] Add missing include, and revert removal of lota --- src/helpers/TxtDataHelpers.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index f2621c0d..4358c8b8 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -28,6 +28,10 @@ bool StrHelper::isBlank(const char* str) { #include #include +#if __has_include() +// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084 +#include +#endif union int32_Float_t { @@ -103,7 +107,9 @@ static void _ftoa(float f, char *p, int *status) *p++ = '0'; else { - p += snprintf(p, 11, "%ld", (long)int_part); + ltoa(int_part, p, 10); + while (*p) + p++; } *p++ = '.'; if (frac_part == 0) From 0a15c487d8b8333dfdc5324deca1a56bb8c8eba0 Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 29 Aug 2026 17:57:06 -0400 Subject: [PATCH 141/174] m4: inital port --- boards/thinknode_m4.json | 72 ++++++++++++ variants/thinknode_m4/ThinkNodeM4Board.cpp | 46 ++++++++ variants/thinknode_m4/ThinkNodeM4Board.h | 61 ++++++++++ variants/thinknode_m4/platformio.ini | 130 +++++++++++++++++++++ variants/thinknode_m4/target.cpp | 84 +++++++++++++ variants/thinknode_m4/target.h | 26 +++++ variants/thinknode_m4/variant.cpp | 94 +++++++++++++++ variants/thinknode_m4/variant.h | 106 +++++++++++++++++ 8 files changed, 619 insertions(+) create mode 100644 boards/thinknode_m4.json create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.cpp create mode 100644 variants/thinknode_m4/ThinkNodeM4Board.h create mode 100644 variants/thinknode_m4/platformio.ini create mode 100644 variants/thinknode_m4/target.cpp create mode 100644 variants/thinknode_m4/target.h create mode 100644 variants/thinknode_m4/variant.cpp create mode 100644 variants/thinknode_m4/variant.h diff --git a/boards/thinknode_m4.json b/boards/thinknode_m4.json new file mode 100644 index 00000000..babc3e52 --- /dev/null +++ b/boards/thinknode_m4.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_thinknode_m4", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M4", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow nrf", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/variants/thinknode_m4/ThinkNodeM4Board.cpp b/variants/thinknode_m4/ThinkNodeM4Board.cpp new file mode 100644 index 00000000..4a144a3a --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.cpp @@ -0,0 +1,46 @@ +#include +#include "ThinkNodeM4Board.h" +#include + +#include + +void ThinkNodeM4Board::begin() { + NRF52Board::begin(); + btn_prev_state = HIGH; + + Wire.begin(); + battery_serial->begin(4800); + + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM4Board::getBattMilliVolts() { + int tentatives = 10; + uint8_t data[5]; + uint8_t b; + + if (battery_serial->available() < 10) + return bat_level_mv; + + // discard old data + while (battery_serial->available() > 10) + battery_serial->read(); + + // synchronize + while((b = battery_serial->read()) != 0xFE) { + if (tentatives-- == 0) { + MESH_DEBUG_PRINTLN("Could not find battery frame start %x", b); + return bat_level_mv; + } + } + battery_serial->readBytes(data, 5); + + if (data[4] != 0xFD) { + MESH_DEBUG_PRINTLN("Invalid battery frame end %x", data[4]); + return bat_level_mv; + } + bat_level_percent = data[0]; + //MESH_DEBUG_PRINTLN("Battery level %d\%", bat_level_percent); + bat_level_mv = 2 * (data[1]*1000. + data[2]*10. + data[3]/10.); + return bat_level_mv; +} diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h new file mode 100644 index 00000000..ba7d95be --- /dev/null +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +class ThinkNodeM4Board : public NRF52BoardDCDC { +protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + uint8_t btn_prev_state; + HardwareSerial * battery_serial; + uint16_t bat_level_mv = 0; + uint8_t bat_level_percent =0; + +public: + ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + void begin(); + uint16_t getBattMilliVolts() override; + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off + } +#endif + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode M4"; + } + + int buttonStateChanged() { + #ifdef BUTTON_PIN + uint8_t v = digitalRead(BUTTON_PIN); + if (v != btn_prev_state) { + btn_prev_state = v; + return (v == USER_BTN_PRESSED) ? 1 : -1; + } + #endif + return 0; + } + + void shutdownPeripherals() override { + // shutdown common peripherals + NRF52Board::shutdownPeripherals(); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + digitalWrite(PIN_PWR_EN, LOW); + digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); + digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + } +}; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini new file mode 100644 index 00000000..ab8417e9 --- /dev/null +++ b/variants/thinknode_m4/platformio.ini @@ -0,0 +1,130 @@ +[ThinkNode_M4] +extends = nrf52_base +board = thinknode_m4 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/thinknode_m4 + -I src/helpers/ui + -D THINKNODE_M4 + -D PIN_USER_BTN=12 + -D PIN_STATUS_LED=LED_STATUS + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D RF_SWITCH_TABLE + -D RX_BOOSTED_GAIN=true + -D P_LORA_BUSY=26 + -D P_LORA_SCLK=6 + -D P_LORA_NSS=27 + -D P_LORA_DIO_1=12 + -D P_LORA_MISO=8 + -D P_LORA_MOSI=7 + -D P_LORA_RESET=40 + -D P_LORA_TX_LED=LED_TX + -D LR11X0_DIO_AS_RF_SWITCH=true + -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 + -D ENV_INCLUDE_GPS=1 +; -D MESH_DEBUG=1 +; -D GPS_NMEA_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/thinknode_m4> + + +debug_tool = stlink +upload_protocol = nrfutil +lib_deps= ${nrf52_base.lib_deps} + +[env:ThinkNode_M4_repeater] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_room_server] +extends = ThinkNode_M4 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D ADVERT_NAME='"ThinkNode_M4 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D RF_SWITCH_TABLE +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + +[env:ThinkNode_M4_companion_radio_usb] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_companion_radio_ble] +extends = ThinkNode_M4 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 708608 +build_flags = ${ThinkNode_M4.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_TX_POWER=0 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=23 + -D PIN_BUZZER_EN=36 +build_src_filter = ${ThinkNode_M4.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M4.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:ThinkNode_M4_kiss_modem] +extends = ThinkNode_M4 +build_src_filter = ${ThinkNode_M4.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = ${ThinkNode_M4.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 diff --git a/variants/thinknode_m4/target.cpp b/variants/thinknode_m4/target.cpp new file mode 100644 index 00000000..7da3f400 --- /dev/null +++ b/variants/thinknode_m4/target.cpp @@ -0,0 +1,84 @@ +#include +#include "target.h" +#include + +ThinkNodeM4Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +#ifdef ENV_INCLUDE_GPS +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); +EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else +EnvironmentSensorManager sensors = EnvironmentSensorManager(); +#endif + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +#ifdef RF_SWITCH_TABLE +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW , LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW , HIGH }}, + { LR11x0::MODE_TX_HF, {LOW , LOW }}, + { LR11x0::MODE_GNSS, {LOW , LOW }}, + { LR11x0::MODE_WIFI, {LOW , LOW }}, + END_OF_MODE_TABLE, +}; +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(2); + radio.explicitHeader(); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/thinknode_m4/target.h b/variants/thinknode_m4/target.h new file mode 100644 index 00000000..1135f5e8 --- /dev/null +++ b/variants/thinknode_m4/target.h @@ -0,0 +1,26 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "ThinkNodeM4Board.h" +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern ThinkNodeM4Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp new file mode 100644 index 00000000..7bf17bb8 --- /dev/null +++ b/variants/thinknode_m4/variant.cpp @@ -0,0 +1,94 @@ +/* + * variant.cpp + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02 + 3, // P0.03 + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 + 12, // P0.12 + 13, // P0.13 + 14, // P0.14 + 15, // P0.15 + 16, // P0.16 + 17, // P0.17 + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24 + 25, // P0.25 + 26, // P0.26 + 27, // P0.27 + 28, // P0.28 + 29, // P0.29 + 30, // P0.30 + 31, // P0.31 + 32, // P1.00 + 33, // P1.01 + 34, // P1.02 + 35, // P1.03 + 36, // P1.04 + 37, // P1.05 + 38, // P1.06 + 39, // P1.07 + 40, // P1.08 + 41, // P1.09 + 42, // P1.10 + 43, // P1.11 + 44, // P1.12 + 45, // P1.13 + 46, // P1.14 + 47, // P1.15 +}; + +void initVariant() +{ +/* TODO */ + pinMode(PIN_PWR_EN, OUTPUT); + pinMode(I2C_POWER, OUTPUT); + + digitalWrite(PIN_PWR_EN, HIGH); + digitalWrite(I2C_POWER, I2C_POWER_ACTIVE); + + pinMode(LED_STATUS, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(LED_BAT1, OUTPUT); + pinMode(LED_BAT2, OUTPUT); + pinMode(LED_BAT3, OUTPUT); + pinMode(LED_BAT4, OUTPUT); + + digitalWrite(LED_BAT1, LOW); + digitalWrite(LED_BAT2, LOW); + digitalWrite(LED_BAT3, LOW); + digitalWrite(LED_BAT4, LOW); + digitalWrite(LED_STATUS, LOW); + digitalWrite(LED_PIN, LOW); + + pinMode(BUTTON_PIN, INPUT_PULLUP); + + pinMode(PIN_GPS_POWER, OUTPUT); + pinMode(PIN_GPS_EN, OUTPUT); + + // Power on gps but in standby + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); +} diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h new file mode 100644 index 00000000..cc483184 --- /dev/null +++ b/variants/thinknode_m4/variant.h @@ -0,0 +1,106 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define NRF_APM // detect usb power + +#define EXT_CHRG_DETECT (38) +// Power to radio +#define PIN_PWR_EN (11) + +// I2C bus power +#define I2C_POWER (32) +#define I2C_POWER_ACTIVE LOW + +#define PIN_BAT_RX (5) +#define PIN_BAT_TX (30) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define PIN_SERIAL2_RX PIN_BAT_TX +#define PIN_SERIAL2_TX PIN_BAT_RX + +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (23) +#define PIN_WIRE_SCL (25) +#define I2C_NO_RESCAN + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (8) +#define PIN_SPI_MOSI (7) +#define PIN_SPI_SCK (6) +#define PIN_SPI_NSS (27) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_BLUE (-1) // disable blue led +#define LED_STATUS (13) // blue +#define LED_PIN (41) // red +#define LED_TX LED_PIN +#define LED_BUILTIN LED_BLUE +#define LED_STATE_ON HIGH + +#define LED_BAT1 (15) +#define LED_BAT2 (17) +#define LED_BAT3 (34) +#define LED_BAT4 (36) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (4) +#define BUTTON_PIN PIN_BUTTON1 +#define USER_BTN_PRESSED HIGH + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define PIN_GPS_RX (44) +#define PIN_GPS_TX (46) + +#define PIN_GPS_POWER (14) +#define GPS_POWER_ACTIVE LOW +#define PIN_GPS_EN (43) +#define GPS_EN_ACTIVE LOW +#define PIN_GPS_RESET (3) +#define GPS_RESET_ACTIVE HIGH From 8bf072a6f011d841cd6c0a4168aba85e5ad6d2ae Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 20 Sep 2026 16:13:15 -0400 Subject: [PATCH 142/174] M4: Handle poweroff --- variants/thinknode_m4/ThinkNodeM4Board.h | 14 ++++++++++++++ variants/thinknode_m4/platformio.ini | 2 +- variants/thinknode_m4/variant.h | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index ba7d95be..c05de911 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -47,6 +47,16 @@ public: // shutdown common peripherals NRF52Board::shutdownPeripherals(); + #ifdef LED_STATUS + digitalWrite(LED_STATUS, HIGH); + #endif + #ifdef BUTTON_PIN + while(!digitalRead(BUTTON_PIN)); + #endif + #ifdef LED_STATUS + digitalWrite(LED_STATUS, LOW); + #endif + digitalWrite(LED_BAT1, LOW); digitalWrite(LED_BAT2, LOW); digitalWrite(LED_BAT3, LOW); @@ -57,5 +67,9 @@ public: digitalWrite(PIN_PWR_EN, LOW); digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE); digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE); + + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + #endif } }; diff --git a/variants/thinknode_m4/platformio.ini b/variants/thinknode_m4/platformio.ini index ab8417e9..06762f88 100644 --- a/variants/thinknode_m4/platformio.ini +++ b/variants/thinknode_m4/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/thinknode_m4 -I src/helpers/ui -D THINKNODE_M4 - -D PIN_USER_BTN=12 + -D PIN_USER_BTN=4 -D PIN_STATUS_LED=LED_STATUS -D RADIO_CLASS=CustomLR1110 -D WRAPPER_CLASS=CustomLR1110Wrapper diff --git a/variants/thinknode_m4/variant.h b/variants/thinknode_m4/variant.h index cc483184..6e355d7f 100644 --- a/variants/thinknode_m4/variant.h +++ b/variants/thinknode_m4/variant.h @@ -88,7 +88,7 @@ #define PIN_BUTTON1 (4) #define BUTTON_PIN PIN_BUTTON1 -#define USER_BTN_PRESSED HIGH +#define USER_BTN_PRESSED LOW //////////////////////////////////////////////////////////////////////////////// // GPS From 84f1e11d29823d51a0047beb07d76b9523093ce5 Mon Sep 17 00:00:00 2001 From: Ev Lbibass <27320050+lbibass@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:38:05 -0400 Subject: [PATCH 143/174] update structure of LR1121 to match LR1110 files. Implement DC-DC on LR1121 radio. saves about 30% power consumption at idle. --- src/helpers/radiolib/CustomLR1121.h | 74 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1121Wrapper.h | 3 + 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 873bfabc..67e3b52a 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -4,11 +4,25 @@ #include "MeshCore.h" class CustomLR1121 : public LR1121 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: CustomLR1121(Module *mod) : LR1121(mod) { } + int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7, + uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10, + uint16_t preambleLength = 8, float tcxoVoltage = 1.6) { + int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength, + tcxoVoltage); + // RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator. + if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC(); + return state; + } + size_t getPacketLength(bool update) override { size_t len = LR1121::getPacketLength(update); if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) { @@ -31,11 +45,61 @@ class CustomLR1121 : public LR1121 { bool getRxBoostedGainMode() const { return _rx_boosted; } - bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags. + return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } - uint8_t getSpreadingFactor() const { return spreadingFactor; } + bool isReceiving() { + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + + uint8_t getSpreadingFactor() const { return spreadingFactor; } }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR1121Wrapper.h b/src/helpers/radiolib/CustomLR1121Wrapper.h index 5361ee23..5a754cee 100644 --- a/src/helpers/radiolib/CustomLR1121Wrapper.h +++ b/src/helpers/radiolib/CustomLR1121Wrapper.h @@ -14,6 +14,9 @@ public: ((CustomLR1121 *)_radio)->setBandwidth(bw); ((CustomLR1121 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } From b5787757a7666b311f016263fed280bd94c22b9e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 21 Sep 2026 15:55:44 +0200 Subject: [PATCH 144/174] Thinknode m4 - don't touch crystal and OTA name --- variants/thinknode_m4/ThinkNodeM4Board.h | 2 +- variants/thinknode_m4/variant.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index c05de911..c2cae54b 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -15,7 +15,7 @@ protected: uint8_t bat_level_percent =0; public: - ThinkNodeM4Board() : NRF52Board("THINKNODE_M3_OTA"), battery_serial(&Serial2) {} + ThinkNodeM4Board() : NRF52Board("THINKNODE_M4_OTA"), battery_serial(&Serial2) {} void begin(); uint16_t getBattMilliVolts() override; diff --git a/variants/thinknode_m4/variant.cpp b/variants/thinknode_m4/variant.cpp index 7bf17bb8..fbdb08e2 100644 --- a/variants/thinknode_m4/variant.cpp +++ b/variants/thinknode_m4/variant.cpp @@ -10,8 +10,8 @@ const uint32_t g_ADigitalPinMap[] = { - 0, // P0.00 - 1, // P0.01 + 0xff, // P0.00 - LFXO (do not use) + 0xff, // P0.01 - LFXO (do not use) 2, // P0.02 3, // P0.03 4, // P0.04 From 0c3fe356d06ea6b9eb6df719c9647ee3664c821d Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Mon, 21 Sep 2026 12:26:36 -0400 Subject: [PATCH 145/174] Constrain `agc.reset.interval` and allow intervals > 255s --- docs/cli_commands.md | 2 +- examples/companion_radio/NodePrefs.h | 4 ++-- src/helpers/CommonCLI.h | 4 ++-- src/helpers/CommonRadioPrefs.cpp | 4 +++- src/helpers/CommonRadioPrefs.h | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f4a43fad..9fde4d43 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -628,7 +628,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set agc.reset.interval ` **Parameters:** -- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable. +- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable. **Default:** `0.0` diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4b169d73..46a3dbb7 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -106,8 +106,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8591cdc1..2759b0c4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -119,8 +119,8 @@ private: void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } - uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } - void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } + uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } + void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); } uint8_t getHashMode() const override { return _parent->path_hash_mode; } void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); } uint8_t getMultiAcks() const override { return _parent->multi_acks; } diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 9d92cad9..41b17993 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -165,7 +165,9 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } if (memcmp(command, "set agc.reset.interval ", 23) == 0) { - setAgcResetInt(atoi(&command[23])); + int secs = atoi(&command[23]); + secs = constrain(secs, 0, 255 * 4); + setAgcResetInt((uint16_t) secs); sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); return true; } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index 96895bb2..0e83ca9d 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -41,8 +41,8 @@ public: virtual float getRxDelay() const = 0; virtual void setRxDelay(float d) = 0; - virtual uint8_t getAgcResetInt() const = 0; - virtual void setAgcResetInt(uint8_t secs) = 0; + virtual uint16_t getAgcResetInt() const = 0; + virtual void setAgcResetInt(uint16_t secs) = 0; virtual uint8_t getHashMode() const = 0; virtual void setHashMode(uint8_t m) = 0; From 61be70849a9a2f123b4b1e0146a1ee05a261ddb5 Mon Sep 17 00:00:00 2001 From: Wolfram Keil Date: Thu, 27 Aug 2026 21:38:46 +0200 Subject: [PATCH 146/174] Add board loop hook for periodic work --- examples/simple_repeater/main.cpp | 2 ++ examples/simple_sensor/main.cpp | 2 ++ src/MeshCore.h | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1f71da74..d475b13d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -165,6 +165,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + #ifdef ETHERNET_ENABLED ethernet_loop_maintain(); if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 749ff6ef..db1c5279 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -153,6 +153,8 @@ void loop() { command[0] = 0; // reset command buffer } + board.loop(); // let the board feed its watchdog, run periodic housekeeping + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/src/MeshCore.h b/src/MeshCore.h index 4bc9d3db..2a3582de 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -76,6 +76,10 @@ public: virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; } + + // Called from the example main loops. Lets a board feed its watchdog and + // run periodic housekeeping. Default no-op. + virtual void loop() { /* no op */ } }; /** From 560fb847d580a2cc97d29ab531c2a0273e20fefc Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 22 Sep 2026 18:01:15 +1000 Subject: [PATCH 147/174] missing board.loop() in companion, room server, kiss modem. --- examples/companion_radio/main.cpp | 1 + examples/kiss_modem/main.cpp | 4 +++- examples/simple_room_server/main.cpp | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4bc75a81..839d4958 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -297,6 +297,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 5836a694..44b2a3df 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,7 +113,7 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) && ARDUINO_USB_MODE +#if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); #endif @@ -149,6 +149,8 @@ void loop() { } } + board.loop(); + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { radio_driver.triggerNoiseFloorCalibrate(0); next_noise_floor_calib_ms = millis(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 227ee2cb..65fc55ef 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -162,6 +162,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.loop(); #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif From c13b31218869f11c8b6e47f16d96b9bab9329095 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 23 Sep 2026 15:04:51 +1200 Subject: [PATCH 148/174] fix heltec ct62 boot loop --- variants/heltec_ct62/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 401a30d2..f98f8d4a 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -1,6 +1,7 @@ [Heltec_ct62] extends = esp32_base board = esp32-c3-devkitm-1 +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32_base.build_flags} -I variants/heltec_ct62 From f9e70426ae5eed7782edd9c6b56af86180e0c63c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 24 Sep 2026 13:35:27 +1000 Subject: [PATCH 149/174] increased MAX_SEARCH_RESULTS to 24. (was 8) Closes #3243 --- examples/simple_sensor/SensorMesh.h | 2 +- src/helpers/BaseChatMesh.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6b7a32fe..8b421081 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -43,7 +43,7 @@ #define FIRMWARE_ROLE "sensor" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MAX_CONCURRENT_ALERTS 4 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 0a8fdef4..835384b7 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -9,7 +9,7 @@ #include "ContactInfo.h" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MSG_SEND_FAILED 0 #define MSG_SEND_SENT_FLOOD 1 From b204f9a114db7b30e166fcb4f6db3b30424f2f55 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 24 Sep 2026 13:49:57 +1000 Subject: [PATCH 150/174] * changed onUnhandledResponse() to return true if handled/consumed * unhandled responses (eg. push telemetry) now sent to app as PUSH_CODE_BINARY_RESPONSE frames --- examples/companion_radio/MyMesh.cpp | 17 +++++++++++++++-- examples/companion_radio/MyMesh.h | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 66efc6b0..f32ea67a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -758,8 +758,21 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); - } else if (_listener && len > 4) { - _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); + } else { + bool handled; + if (_listener && len > 4) { + handled = _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); + } else { + handled = false; + } + if (!handled) { // let the app know about unknown/unhandled responses (eg. push telemetry packets) + int i = 0; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], data, len); + i += len; + _serial->writeFrame(out_frame, i); + } } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 9f077c30..52a22cb4 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -101,7 +101,7 @@ public: virtual void onACKRecv(uint32_t ack_crc) { } virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } - virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual bool onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { return false; /* not handled */ } virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } virtual void onRawDataRecv(mesh::Packet *pkt) { } From aa1c46e1db1c24a828a896db8b7ed36047cf1515 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 25 Sep 2026 16:55:17 +1200 Subject: [PATCH 151/174] disable hibernate ui on 1watt wio tracker variant --- variants/wio-tracker-l1-1w/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/wio-tracker-l1-1w/platformio.ini b/variants/wio-tracker-l1-1w/platformio.ini index e5b4a3be..7d3c2dc3 100644 --- a/variants/wio-tracker-l1-1w/platformio.ini +++ b/variants/wio-tracker-l1-1w/platformio.ini @@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1-1W.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -96,6 +97,7 @@ build_flags = ${WioTrackerL1-1W.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D RADIO_FEM_TXGAIN=1 From d07803bd9bff45a2ba9b6343de81ad1a56edef78 Mon Sep 17 00:00:00 2001 From: Marcel Verdult Date: Sat, 26 Sep 2026 14:07:28 +0200 Subject: [PATCH 152/174] Fix RAK11310 build: add no-op attachDynamicPrefs() --- variants/rak11310/RAK11310Board.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/variants/rak11310/RAK11310Board.h b/variants/rak11310/RAK11310Board.h index ea0f15e2..5b75ac36 100644 --- a/variants/rak11310/RAK11310Board.h +++ b/variants/rak11310/RAK11310Board.h @@ -2,6 +2,7 @@ #include #include +#include // from https://github.com/RAKWireless/RAK11300-AT-Command-Firmware/blob/9c48409a43620a828d653501d536473200aa33af/RAK11300-AT-Arduino/batt.cpp#L17-L19 #define VBAT_MV_PER_LSB (0.806F) // 3.0V ADC range and 12 - bit ADC resolution = 3300mV / 4096 @@ -46,4 +47,6 @@ public: void reboot() override { rp2040.reboot(); } bool startOTAUpdate(const char *id, char reply[]) override; + + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op }; From 3fa8dbf4d10e780bec9ec0e5de3b1807e828830e Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 29 Sep 2026 20:10:51 +1000 Subject: [PATCH 153/174] feat: add waitFor() and drain() to LocationProvider to support sending commands to GPS --- src/helpers/sensors/LocationProvider.h | 2 ++ .../sensors/MicroNMEALocationProvider.h | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652..e11b738b 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -17,6 +17,8 @@ public: virtual bool isValid() = 0; virtual long getTimestamp() = 0; virtual void sendSentence(const char * sentence); + virtual bool waitFor(const char* prefix, uint32_t timeout_ms) { return false; } + virtual void drain() { } virtual void reset() = 0; virtual void begin() = 0; virtual void stop() = 0; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb114..e559ad01 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -129,10 +129,31 @@ public : return dt.unixtime(); } + void drain() { while (_gps_serial->available()) _gps_serial->read(); } + + const char* getSentence() const { return nmea.getSentence(); } + void sendSentence(const char *sentence) override { nmea.sendSentence(*_gps_serial, sentence); } + bool waitFor(const char* prefix, uint32_t timeout_ms) { + size_t plen = strlen(prefix); + uint32_t timeout = millis() + timeout_ms; + while ((int32_t)(millis() - timeout) < 0) { + if (_gps_serial->available()) { + char c = _gps_serial->read(); + #ifdef GPS_NMEA_DEBUG + Serial.print(c); + #endif + if (nmea.process(c) && strncmp(nmea.getSentence(), prefix, plen) == 0) return true; + } else { + yield(); + } + } + return false; + } + void loop() override { while (_gps_serial->available()) { From 037e27833b5edc63cd905bb706c07025819114ec Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 29 Sep 2026 20:12:08 +1000 Subject: [PATCH 154/174] feat: add AirohaSleep.h helper for faster fix on AG3335 based GPS --- src/helpers/sensors/AirohaSleep.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/helpers/sensors/AirohaSleep.h diff --git a/src/helpers/sensors/AirohaSleep.h b/src/helpers/sensors/AirohaSleep.h new file mode 100644 index 00000000..6637d0fb --- /dev/null +++ b/src/helpers/sensors/AirohaSleep.h @@ -0,0 +1,19 @@ +#pragma once +#include + +// Board must keep the GPS RTC backup pin powered and can cut GPS power once we return. +// Returns true if the command was acked within 3 attempts, otherwise returns false. +static inline bool airohaEnterSleep(LocationProvider* nmea) { + for (uint8_t attempt = 0; attempt < 3; attempt++) { + nmea->drain(); + nmea->sendSentence("$PAIR650,0"); + if (nmea->waitFor("$PAIR001,650,0", 50)) { // wait for the command received signal + #ifdef GPS_NMEA_DEBUG + Serial.printf("Airoha RTC Backup sleep command accepted by GPS after %u attempts\r\n", attempt + 1); + #endif + nmea->waitFor("$PAIR650,0", 50); // give the GPS 50ms grace to signal it is ready for sleep + return true; + } + } + return false; +} \ No newline at end of file From 11755c81996fc8cd76708d2a420aaee920a58b02 Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 29 Sep 2026 20:14:19 +1000 Subject: [PATCH 155/174] MeshTracker X1 GPS: use RTC backup for faster fix --- variants/meshtracker_x1/target.cpp | 38 ++++++++---------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/variants/meshtracker_x1/target.cpp b/variants/meshtracker_x1/target.cpp index d6d548b5..5b90d660 100644 --- a/variants/meshtracker_x1/target.cpp +++ b/variants/meshtracker_x1/target.cpp @@ -1,6 +1,7 @@ #include #include "target.h" #include +#include MeshTrackerX1Board board; @@ -27,46 +28,27 @@ mesh::LocalIdentity radio_new_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, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); } void MeshTrackerX1SensorManager::stop_gps() { gps_active = false; - digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_VRTC_EN, HIGH); // keep GPS RTC alive for faster fix on wake + digitalWrite(GPS_RTC_INT, LOW); // make sure this is LOW so we can pulse it to wake + airohaEnterSleep(_nmea); 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); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); // init SPA06-003 barometer baro_ok = spa06.begin(SPA06_003_DEFAULT_ADDR, &Wire) || spa06.begin(0x76, &Wire); @@ -122,7 +104,7 @@ const char* MeshTrackerX1SensorManager::getSettingValue(int i) const { 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 ! + stop_gps(); } else { start_gps(); } From 74413f0e4df005d3d776027439a8e1f15fd509a4 Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 29 Sep 2026 20:16:32 +1000 Subject: [PATCH 156/174] T1000e GPS: use RTC backup for faster fix --- variants/t1000-e/target.cpp | 48 ++++++++---------------------------- variants/t1000-e/variant.cpp | 8 +++--- 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index 42532827..70a64359 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -2,6 +2,7 @@ #include "t1000e_sensors.h" #include "target.h" #include +#include T1000eBoard board; @@ -81,56 +82,28 @@ mesh::LocalIdentity radio_new_identity() { void T1000SensorManager::start_gps() { gps_active = true; - //_nmea->begin(); - // this init sequence should be better - // 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, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, INPUT_PULLUP); -} - -void T1000SensorManager::sleep_gps() { - gps_active = false; - digitalWrite(GPS_VRTC_EN, HIGH); - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); } void T1000SensorManager::stop_gps() { gps_active = false; - digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_VRTC_EN, HIGH); // keep GPS RTC alive for faster fix on wake + digitalWrite(GPS_RTC_INT, LOW); // make sure this is LOW so we can pulse it to wake + airohaEnterSleep(_nmea); // send command to put the GPS into RTC backup sleep digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); } bool T1000SensorManager::begin() { // init GPS Serial1.begin(115200); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); return true; } @@ -156,7 +129,6 @@ void T1000SensorManager::loop() { node_lat = ((double)_nmea->getLatitude())/1000000.; node_lon = ((double)_nmea->getLongitude())/1000000.; node_altitude = ((double)_nmea->getAltitude()) / 1000.0; - //Serial.printf("lat %f lon %f\r\n", _lat, _lon); } next_gps_update = millis() + 1000; } @@ -176,7 +148,7 @@ const char* T1000SensorManager::getSettingValue(int i) const { bool T1000SensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { - sleep_gps(); // sleep for faster fix ! + stop_gps(); } else { start_gps(); } diff --git a/variants/t1000-e/variant.cpp b/variants/t1000-e/variant.cpp index ed21fd68..fc30a619 100644 --- a/variants/t1000-e/variant.cpp +++ b/variants/t1000-e/variant.cpp @@ -71,7 +71,7 @@ void initVariant() pinMode(LUX_SENSOR, INPUT); pinMode(EXT_CHRG_DETECT, INPUT); pinMode(EXT_PWR_DETECT, INPUT); - pinMode(GPS_RESETB, INPUT); + pinMode(GPS_RESETB, INPUT_PULLUP); pinMode(PIN_BUTTON1, INPUT); pinMode(PIN_3V3_EN, OUTPUT); @@ -89,10 +89,10 @@ void initVariant() digitalWrite(PIN_3V3_ACC_EN, LOW); digitalWrite(BUZZER_EN, LOW); digitalWrite(SENSOR_EN, LOW); - digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_EN, HIGH); digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_SLEEP_INT, LOW); digitalWrite(GPS_RTC_INT, LOW); digitalWrite(LED_PIN, LOW); } From a11009f2dffa1bbbe428a970049d7b87882cff38 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 30 Sep 2026 14:52:58 +1300 Subject: [PATCH 157/174] add radio preset docs --- docs/radio_presets.md | 66 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/radio_presets.md diff --git a/docs/radio_presets.md b/docs/radio_presets.md new file mode 100644 index 00000000..ea3b07e5 --- /dev/null +++ b/docs/radio_presets.md @@ -0,0 +1,66 @@ +# Radio Presets + +The radio presets are currently stored on the MeshCore App API server. + +This allows us to update the presets without releasing new application updates. + +The presets can be fetched from [https://api.meshcore.nz/api/v1/config](https://api.meshcore.nz/api/v1/config) + +If you'd like a new radio preset to be added, this will need to be brought up for discussion in the [MeshCore Discord](https://meshcore.gg) server, or via the [issues section](https://github.com/meshcore-dev/MeshCore/issues) in our GitHub repo. + +Before a new preset will be considered for being added, local radio regulations must be researched by the person suggesting the preset, and investigation of the frequencies with an SDR is advised to ensure the least interference to provide a decent user experience. + +Once a preset is added, it's unlikely to be changed. Adding too many presets can lead to user frustration, as new users won't know which preset to select during setup. + +At this time, presets provided by the MeshCore App API are just suggestions, but you are free to configure any radio settings through the companion applications, provided they comply with local laws/regulations. + +> Note: We are planning to implement deep linking such as meshcore:// URLs, and QR codes so local mesh networks can easily distribute their own settings, without relying on the internet, or all the companion applications to provide all the settings. + +## JSON Response + +See the example JSON response below, which shows the expected format provided by the API server. + +Your application should be programmed to expect any of the JSON keys to be missing from the response, as the format could change at any time without any notification. + +Please configure an appropriate User Agent header string when sending your request to the server. We should be able to reach out to you if needed. + +> Note: Some presets contain an optional `network_settings` section which defines the `path_hash_size` to use for this mesh network. + +```json +{ + "config": { + "suggested_radio_settings": { + "info_message": "These presets are suggested by the community.", + "entries": [ + { + "title": "Australia (Narrow)", + "description": "916.575MHz / SF7 / BW62.5 / CR7", + "frequency": "916.575", + "spreading_factor": "7", + "bandwidth": "62.5", + "coding_rate": "7" + }, + { + "title": "New Zealand (Narrow)", + "description": "917.375MHz / SF7 / BW62.5 / CR5 / 2B", + "frequency": "917.375", + "spreading_factor": "7", + "bandwidth": "62.5", + "coding_rate": "5", + "network_settings": { + "path_hash_size": 2 + } + }, + { + "title": "Portugal 868", + "description": "869.618MHz / SF7 / BW62.5 / CR6", + "frequency": "869.618", + "spreading_factor": "7", + "bandwidth": "62.5", + "coding_rate": "6" + } + ] + } + } +} +``` \ No newline at end of file From a706d1d943717090030ee395ad846e62a4d6cfb3 Mon Sep 17 00:00:00 2001 From: Huw Duddy <37787853+oltaco@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:17:44 +1000 Subject: [PATCH 158/174] Revert "Fix Heltec Mesh Node T1 GNSS pins" --- variants/heltec_t1/variant.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_t1/variant.h b/variants/heltec_t1/variant.h index 70ec23f7..de40ea64 100644 --- a/variants/heltec_t1/variant.h +++ b/variants/heltec_t1/variant.h @@ -137,8 +137,8 @@ #define GPS_EN_ACTIVE LOW #define PIN_GPS_EN_ACTIVE LOW #define PIN_GPS_PPS (32 + 9) -#define GPS_RX_PIN (0 + 8) -#define GPS_TX_PIN (0 + 7) +#define GPS_TX_PIN (0 + 8) +#define GPS_RX_PIN (0 + 7) #define PIN_GPS_TX GPS_TX_PIN #define PIN_GPS_RX GPS_RX_PIN From 35e10a9c75d71c8859369507a259b4dcb1efd813 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 2 Oct 2026 07:35:34 +1000 Subject: [PATCH 159/174] fix bad comment format --- src/helpers/nrf52/SerialBLEInterface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 2e5626a8..fa091c03 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -129,10 +129,10 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code char charpin[20]; snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code); - #ifdef DISABLE_BLE_LED \\ use variant platformio.ini build_flags macro: -D DISABLE_BLE_LED - Bluefruit.autoConnLed(false); \\ false removes the automatic control of the LED by the Bluefruit library + #ifdef DISABLE_BLE_LED // use variant platformio.ini build_flags macro: -D DISABLE_BLE_LED + Bluefruit.autoConnLed(false); // false removes the automatic control of the LED by the Bluefruit library #else - Bluefruit.autoConnLed(true); \\ true leaves the default automatic control of the LED by the Bluefruit library + Bluefruit.autoConnLed(true); // true leaves the default automatic control of the LED by the Bluefruit library #endif // If we want to control BLE LED ourselves, uncomment this: From 0e1ba7102d776f3214e69c13239bb73bddaa618f Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 2 Oct 2026 07:40:42 +1000 Subject: [PATCH 160/174] tidy: remove old commented Bluefruit.autoConnLed(false) --- src/helpers/nrf52/SerialBLEInterface.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index fa091c03..6ed16bc8 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -135,8 +135,6 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code Bluefruit.autoConnLed(true); // true leaves the default automatic control of the LED by the Bluefruit library #endif - // If we want to control BLE LED ourselves, uncomment this: - // Bluefruit.autoConnLed(false); Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.begin(); From f19733e4787933e67d05e1105ce1372e3bf771f4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 4 Oct 2026 15:02:00 +1100 Subject: [PATCH 161/174] * don't respond to DISCOVER requests if node has adverts turned off. (isHiddenNode()) --- examples/simple_repeater/MyMesh.cpp | 1 + examples/simple_repeater/MyMesh.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca6a3e60..61b6c149 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -790,6 +790,7 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime()) + && !isHiddenNode() // this node wishes to NOT be discoverable ) { int i = 1; uint8_t filter = packet->payload[i++]; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index cac6c4a2..a9ac453a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -187,6 +187,9 @@ public: NodePrefs* getNodePrefs() { return &_prefs; } + bool isHiddenNode() const { + return _prefs.advert_interval == 0 && _prefs.flood_advert_interval == 0; + } void savePrefs() override { _cli.savePrefs(_fs); From 4c2af58d3e5e25eed9534ff012cd125134234b79 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 6 Oct 2026 18:50:15 +0200 Subject: [PATCH 162/174] Use consistent `_OTA` suffix for nRF52 OTA BLE names --- variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h | 2 +- variants/meshtiny/MeshtinyBoard.h | 2 +- variants/wio-tracker-l1-1w/WioTrackerL1Board.h | 2 +- variants/wio-tracker-l1/WioTrackerL1Board.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 30bbc3a5..f7ef536b 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus OTA") {} + TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index 67a521fe..21673d0a 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - MeshtinyBoard() : NRF52Board("Meshtiny OTA") {} + MeshtinyBoard() : NRF52Board("Meshtiny_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index 798145db..f6164e47 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -14,7 +14,7 @@ protected: bool isLoRaFemPaGainEnabled() const { return _is_pa_enabled; } public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); void attachDynamicPrefs(KeyValueStore* prefs); bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index e376e066..99e852bb 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); #if defined(P_LORA_TX_LED) From 47c3005b08624f55d1ef6848d9af5e458d702143 Mon Sep 17 00:00:00 2001 From: quyenlt298 <61147413+quyenlt298@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:09:36 +0700 Subject: [PATCH 163/174] Add Nelos allocation for GroupData --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index b8cc22a2..79cc90b8 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -19,6 +19,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 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 | +| 0140 | Nelos | support@nelos.app — https://nelos.app | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 01f9293be073937791cdb6594c08d7bcc7f7f3ed Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 8 Oct 2026 19:56:45 +1100 Subject: [PATCH 164/174] * treat transport code 0 the same as un-scoped. --- src/helpers/RegionMap.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 4667e003..d8d77380 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -188,6 +188,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], } RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { + if (packet->transport_codes[0] == 0) return &wildcard; for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) From 6df5aaed4335d41e092f9f9c2516903dc9f451ff Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 12 Oct 2026 10:44:42 +1100 Subject: [PATCH 165/174] * ACK forwarding (in direct mode) had zero delay --- src/Mesh.cpp | 12 ++++++------ src/Mesh.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index c11f37ca..9dc42f0b 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -87,13 +87,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { + uint32_t d = getDirectRetransmitDelay(pkt); if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { - return forwardMultipartDirect(pkt); + return forwardMultipartDirect(pkt, d); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { if (!_tables->wasSeen(pkt)) { // don't retransmit! _tables->markSeen(pkt); removeSelfFromPath(pkt); - routeDirectRecvAcks(pkt, 0); + routeDirectRecvAcks(pkt, d); } return ACTION_RELEASE; } @@ -102,7 +103,6 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { _tables->markSeen(pkt); removeSelfFromPath(pkt); - uint32_t d = getDirectRetransmitDelay(pkt); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } @@ -356,7 +356,7 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { return ACTION_RELEASE; } -DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { +DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt, uint32_t delay_millis) { uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent uint8_t type = pkt->payload[0] & 0x0F; @@ -370,7 +370,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { 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) + routeDirectRecvAcks(&tmp, delay_millis + ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2) } } return ACTION_RELEASE; @@ -380,13 +380,13 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { if (!packet->isMarkedDoNotRetransmit()) { uint8_t extra = getExtraAckTransmitCount(); while (extra > 0) { - delay_millis += getDirectRetransmitDelay(packet) + 300; auto a1 = createMultiAck(packet->payload, packet->payload_len, extra); if (a1) { a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; sendPacket(a1, 0, delay_millis); + delay_millis += 300; } extra--; } diff --git a/src/Mesh.h b/src/Mesh.h index 49a299a6..22bb5c6d 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -32,7 +32,7 @@ class Mesh : public Dispatcher { void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); - DispatcherAction forwardMultipartDirect(Packet* pkt); + DispatcherAction forwardMultipartDirect(Packet* pkt, uint32_t delay_millis); protected: DispatcherAction onRecvPacket(Packet* pkt) override; From 3d1a5d4c64eec2e3310593fe7112f2ca02a05772 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 11 Oct 2026 20:51:18 -0700 Subject: [PATCH 166/174] feat(variants): add Waveshare ESP32-S3-LR1121 ESP32-S3FH4R2 (4MB flash, 2MB PSRAM, native USB-CDC) with an LR1121. Pins, the DIO5/DIO6 RF switch table and the 1.8 V TCXO follow the HF schematic and Waveshare's examples. Envs: repeater, room server, companion USB/BLE and KISS modem, all on min_spiffs so roles share one partition layout. Only RFO_HP_LF is wired (R4 on RFO_LP_LF is not fitted), so the new opt-in LR1121_FORCE_HP_PA flag keeps RadioLib on the HP PA at <= 14 dBm, where it would otherwise select the unconnected LP PA. --- src/helpers/radiolib/CustomLR1121.h | 7 ++ .../WaveshareS3LR1121Board.h | 11 ++ .../waveshare_esp32s3_lr1121/platformio.ini | 102 ++++++++++++++++++ variants/waveshare_esp32s3_lr1121/target.cpp | 63 +++++++++++ variants/waveshare_esp32s3_lr1121/target.h | 17 +++ 5 files changed, 200 insertions(+) create mode 100644 variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h create mode 100644 variants/waveshare_esp32s3_lr1121/platformio.ini create mode 100644 variants/waveshare_esp32s3_lr1121/target.cpp create mode 100644 variants/waveshare_esp32s3_lr1121/target.h diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 67e3b52a..e8c6c431 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -36,6 +36,13 @@ class CustomLR1121 : public LR1121 { return len; } +#ifdef LR1121_FORCE_HP_PA + // boards with only RFO_HP_LF wired: stop RadioLib switching to the LP PA at <= 14 dBm (2.4 GHz still uses the HF PA) + int16_t setOutputPower(int8_t power) override { + return LR1121::setOutputPower(power, true); + } +#endif + float getFreqMHz() const { return freqMHz; } int16_t setRxBoostedGainMode(bool en) { diff --git a/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h new file mode 100644 index 00000000..8307ba50 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class WaveshareS3LR1121Board : public ESP32Board { +public: + const char* getManufacturerName() const override { + return "Waveshare ESP32-S3-LR1121"; + } +}; diff --git a/variants/waveshare_esp32s3_lr1121/platformio.ini b/variants/waveshare_esp32s3_lr1121/platformio.ini new file mode 100644 index 00000000..8e7b52d4 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/platformio.ini @@ -0,0 +1,102 @@ +; Waveshare ESP32-S3-LR1121 (HF: 850-930 MHz + 2.4 GHz) +; ESP32-S3FH4R2 (4MB flash, 2MB PSRAM), native USB-CDC via the Tiny-Adapter FPC. +; No user LED, battery sense or user button (BOOT is GPIO0). +[Waveshare_S3_LR1121] +extends = esp32_base +board = esp32-s3-zero +; min_spiffs: 1.9MB OTA app slots; one layout for every role so switching roles keeps the filesystem +board_build.partitions = min_spiffs.csv +build_flags = ${esp32_base.build_flags} + -I variants/waveshare_esp32s3_lr1121 + -D BOARD_HAS_PSRAM + -D P_LORA_DIO_1=38 + -D P_LORA_NSS=42 + -D P_LORA_RESET=39 + -D P_LORA_BUSY=41 + -D P_LORA_SCLK=40 + -D P_LORA_MISO=46 + -D P_LORA_MOSI=45 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 + -D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC) + -D RX_BOOSTED_GAIN=true + -D LORA_TX_POWER=22 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/waveshare_esp32s3_lr1121> +lib_deps = + ${esp32_base.lib_deps} + +[env:Waveshare_S3_LR1121_repeater] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 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 = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_room_server] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 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 = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_companion_radio_usb] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.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 = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_companion_radio_ble] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_kiss_modem] +extends = Waveshare_S3_LR1121 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/waveshare_esp32s3_lr1121/target.cpp b/variants/waveshare_esp32s3_lr1121/target.cpp new file mode 100644 index 00000000..56775c4c --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.cpp @@ -0,0 +1,63 @@ +#include +#include "target.h" + +WaveshareS3LR1121Board 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; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +// DIO5/DIO6 drive the RTC6603SP sub-GHz T/R switch; the 2.4 GHz port (RFIO_HF) is unswitched +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, +}; + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, + LORA_TX_POWER, 16, LR11X0_DIO3_TCXO_VOLTAGE); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; + } + + radio.setCRC(2); + radio.explicitHeader(); + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/waveshare_esp32s3_lr1121/target.h b/variants/waveshare_esp32s3_lr1121/target.h new file mode 100644 index 00000000..461509e5 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.h @@ -0,0 +1,17 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "WaveshareS3LR1121Board.h" + +extern WaveshareS3LR1121Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); From eac6e66b1bf423e68bdc26cbf1180096c33340ef Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 11 Oct 2026 20:53:23 -0700 Subject: [PATCH 167/174] fix(radiolib): keep LR1121 tx power valid across sub-GHz and 2.4 GHz - Clip requested power to the active PA's range instead of failing. A stored 22 dBm was rejected at 2.4 GHz (HF PA max 13 dBm), and since setTxPower ignores errors the sub-GHz PA stayed configured. - Re-apply the requested power after every successful retune: PA choice depends on the band, and set radio / tempradio only call setParams. --- src/helpers/radiolib/CustomLR1121.h | 30 +++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index e8c6c431..7182d1d5 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -9,6 +9,8 @@ class CustomLR1121 : public LR1121 { uint32_t _activityAt = 0; bool _headerSeen = false; bool _rx_boosted = false; + int8_t _txPower = 0; + bool _txPowerSet = false; public: CustomLR1121(Module *mod) : LR1121(mod) { } @@ -36,12 +38,32 @@ class CustomLR1121 : public LR1121 { return len; } -#ifdef LR1121_FORCE_HP_PA - // boards with only RFO_HP_LF wired: stop RadioLib switching to the LP PA at <= 14 dBm (2.4 GHz still uses the HF PA) + using LR1121::setOutputPower; + using LR1121::setFrequency; + int16_t setOutputPower(int8_t power) override { - return LR1121::setOutputPower(power, true); - } +#ifdef LR1121_FORCE_HP_PA + const bool force_hp = true; // boards with only RFO_HP_LF wired: never select the LP PA +#else + const bool force_hp = false; #endif + // clip to the active PA's range (HF PA tops out at 13 dBm) rather than fail and keep the other band's PA + int8_t clipped = power; + checkOutputPower(power, &clipped, force_hp); + int16_t state = LR1121::setOutputPower(clipped, force_hp); + if (state == RADIOLIB_ERR_NONE) { + _txPower = power; + _txPowerSet = true; + } + return state; + } + + int16_t setFrequency(float freq) override { + int16_t state = LR1121::setFrequency(freq); + // PA selection depends on the band, and set radio / tempradio don't call setTxPower + if (state == RADIOLIB_ERR_NONE && _txPowerSet) state = setOutputPower(_txPower); + return state; + } float getFreqMHz() const { return freqMHz; } From 8579c667472d95d7873ea91a8ff5b219ff48cb85 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 14 Oct 2026 11:48:58 +1100 Subject: [PATCH 168/174] * Room: "set repeat" CLI now deprecated --- examples/simple_room_server/MyMesh.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 71b8d32a..458537e6 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -983,6 +983,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply addSystemPost(msg); snprintf(reply, MAX_POST_TEXT_LEN, "OK"); } + } else if (memcmp(command, "set repeat ", 11) == 0) { + strcpy(reply, "ERR - repeat now deprecated"); } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } From 481f72cae7bf9bee43c3e47cfb21138a1cb34cc0 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 14 Oct 2026 12:21:38 +1100 Subject: [PATCH 169/174] * only 'repeat on' now intercepted --- examples/simple_room_server/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 458537e6..da4c776c 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -983,7 +983,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply addSystemPost(msg); snprintf(reply, MAX_POST_TEXT_LEN, "OK"); } - } else if (memcmp(command, "set repeat ", 11) == 0) { + } else if (memcmp(command, "set repeat on", 13) == 0) { strcpy(reply, "ERR - repeat now deprecated"); } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands From 590bc23ab9bd32be5366be4ae90c8a91677ffffe Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 14 Oct 2026 15:05:00 +1300 Subject: [PATCH 170/174] enable wifi interface for heltec v3 ble variant --- variants/heltec_v3/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a2b159ec..e60e35e0 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -168,12 +168,14 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = From c0d00a060a794bbd4a3acfba8db3b7b91efe95b7 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 14 Oct 2026 15:16:55 +1300 Subject: [PATCH 171/174] enable wifi interface for heltec v4 ble variant --- variants/heltec_v4/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 96b3eec0..76e8e714 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -211,12 +211,14 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = From a0fc769660642c6f3a8371b11e90064f6591b7ba Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 14 Oct 2026 16:29:41 +1300 Subject: [PATCH 172/174] rework companion ui for ip address so it fits in multi interface variants --- examples/companion_radio/ui-new/UITask.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 181bc832..bc25de79 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -281,10 +281,17 @@ public: if (_page == HomePage::FIRST) { display.setColor(UIColor::primary_txt); + #ifdef ENABLE_WIFI_INTERFACE + // smaller unread message ui for wifi variants + display.setTextSize(1); + sprintf(tmp, "Unread Msg: %d", _task->getMsgCount()); + display.drawTextCentered(display.width() / 2, 22, tmp); + #else + // larger unread message ui for non wifi variants display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.drawTextCentered(display.width() / 2, 22, tmp); - + #endif #ifdef UI_SHOW_CLOCK display.setTextSize(3); uint32_t now = _rtc->getCurrentTime(); @@ -300,7 +307,7 @@ public: IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 54, tmp); + display.drawTextCentered(display.width() / 2, 32, tmp); #endif if (_task->hasConnection()) { display.setColor(UIColor::warning_txt); From 97c0aec8d6e2671ba2c64467b408ee2fd79c808f Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Thu, 15 Oct 2026 09:57:31 +0200 Subject: [PATCH 173/174] added dynamic wifi to: heltec wsl3, v4r8, wireless paper; rak 3112; xiao s3 (wio) --- variants/heltec_v3/platformio.ini | 4 +++- variants/heltec_v4_r8/platformio.ini | 2 ++ variants/heltec_wireless_paper/platformio.ini | 4 +++- variants/rak3112/platformio.ini | 4 +++- variants/xiao_s3/platformio.ini | 2 ++ variants/xiao_s3_wio/platformio.ini | 2 ++ 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index e60e35e0..06c02856 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -314,12 +314,14 @@ build_flags = -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 ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 7611cc1c..e22ca8f5 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -160,10 +160,12 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 55ed62b3..56f95b2f 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -52,11 +52,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=E213Display -D BLE_PIN_CODE=123456 ; dynamic, random PIN - -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 5f3b905b..d181388b 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -155,12 +155,14 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; dynamic, random PIN -D AUTO_SHUTDOWN_MILLIVOLTS=3400 - -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE +; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 1632d112..b8ae7973 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -100,12 +100,14 @@ build_flags = -D BLE_PIN_CODE=123456 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 3bb95607..5a2ef106 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -160,12 +160,14 @@ build_flags = -D BLE_PIN_CODE=123456 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From 07a833bbbe7c0431f652e32a835db1205ca412ed Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 15 Oct 2026 19:42:01 +1100 Subject: [PATCH 174/174] * version 1.18.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 52a22cb4..944790df 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -7,11 +7,11 @@ #define FIRMWARE_VER_CODE 14 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "14 Aug 2026" +#define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.17.1" +#define FIRMWARE_VERSION "v1.18.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index a9ac453a..dc893258 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -67,11 +67,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6..4dee8bdb 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -28,11 +28,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 8b421081..590ca0c7 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 "14 Aug 2026" + #define FIRMWARE_BUILD_DATE "15 Oct 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.17.1" + #define FIRMWARE_VERSION "v1.18.0" #endif #define FIRMWARE_ROLE "sensor"