From 04d2dd3b1c330c3b0dbedd7d36eb5be465b399ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:16:47 -0600 Subject: [PATCH 01/31] Update GxEPD2 to v1.6.6 (#9412) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/m5stack_coreink/platformio.ini | 2 +- variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini | 2 +- variants/esp32s3/esp32-s3-pico/platformio.ini | 2 +- variants/esp32s3/t-deck-pro/platformio.ini | 2 +- variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini | 2 +- variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini | 2 +- variants/nrf52840/MakePython_nRF52840_eink/platformio.ini | 2 +- variants/nrf52840/TWC_mesh_v4/platformio.ini | 2 +- variants/nrf52840/rak4631_epaper/platformio.ini | 2 +- variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index a4d44a15e..0a79cab28 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -19,7 +19,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.3 lib_ignore = diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index 95e909b91..f44f26006 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -11,7 +11,7 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.2 build_unflags = diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index 8b65c3ca3..db0c038e6 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -23,6 +23,6 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index b2c91dcf5..aca31b599 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -29,7 +29,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index 880871e13..093c3732d 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index 025e28078..5951a37a3 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index d823a7230..ecb630bb7 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -15,6 +15,6 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index e196029ec..d93f179c2 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 debug_tool = jlink diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index bc21b7912..f0da832cb 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -15,7 +15,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index d0bca377b..112ddfc29 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -17,7 +17,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.5 + zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library From a417760887cbc25bddc5e2bb9204ae1ea45ababf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:18:48 -0600 Subject: [PATCH 02/31] Update meshtastic/device-ui digest to 37ad715 (#9403) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 016bc1b2d..ee51d24ec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -119,7 +119,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/613c0953313bbd236f4ddc5ede447e9edf8e890a.zip + https://github.com/meshtastic/device-ui/archive/37ad715b76cd6ca4aa500a4a4d9740e3cdf3e3cb.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 5838b26d90c37702a95f08f4f59e14a38b2bd5b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:39:05 -0600 Subject: [PATCH 03/31] Update lewisxhe-SensorLib to v0.3.4 (#9395) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/tbeam/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index 51952457a..16a3d1845 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -32,4 +32,4 @@ lib_deps = # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 From c98f134b403e4fd8c523b8e106befc8ddec7ee28 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:39:22 -0600 Subject: [PATCH 04/31] Update meshtastic-esp32_https_server digest to b0f3960 (#9393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/esp32-common.ini | 2 +- variants/esp32/esp32.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 3ee2b9516..1ff4ce4d8 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -59,7 +59,7 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip + https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 502d937b0..2ce9f4d22 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -17,7 +17,7 @@ lib_deps = ${environmental_extra_no_bsec.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip + https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@^1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master From 9faf178bdcaef725f50f3a109031a12441e468be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:45:26 -0600 Subject: [PATCH 05/31] Update XPowersLib to v0.3.3 (#9354) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/esp32-common.ini | 2 +- variants/esp32/esp32.ini | 2 +- variants/esp32c6/esp32c6.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 1ff4ce4d8..bbbcd3cbe 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -65,7 +65,7 @@ lib_deps = # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip + https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 2ce9f4d22..20ce38fae 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -23,6 +23,6 @@ lib_deps = # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip + https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 \ No newline at end of file diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index c1dfa4d28..9ee8591be 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -28,7 +28,7 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@0.3.2 + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto From b627fa720ba5a259bdc0b9254faf4f9f679f31d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:46:46 -0600 Subject: [PATCH 06/31] Update SensorLib to v0.3.4 (#9396) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/m5stack_coreink/platformio.ini | 2 +- variants/esp32s3/t-watch-s3/platformio.ini | 2 +- variants/esp32s3/tbeam-s3-core/platformio.ini | 2 +- variants/esp32s3/tlora-pager/platformio.ini | 2 +- variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini | 2 +- variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini | 2 +- variants/nrf52840/nano-g2-ultra/platformio.ini | 2 +- variants/nrf52840/t-echo/platformio.ini | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index 0a79cab28..af1535f59 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -21,7 +21,7 @@ lib_deps = # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 zinggjm/GxEPD2@1.6.6 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 lib_ignore = m5stack-coreink monitor_filters = esp32_exception_decoder diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 7d7b07ff6..9c7a642b2 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -24,7 +24,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.7 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio diff --git a/variants/esp32s3/tbeam-s3-core/platformio.ini b/variants/esp32s3/tbeam-s3-core/platformio.ini index c0a32c49c..512cf3202 100644 --- a/variants/esp32s3/tbeam-s3-core/platformio.ini +++ b/variants/esp32s3/tbeam-s3-core/platformio.ini @@ -19,7 +19,7 @@ board_check = true lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 5973db1d0..ea6369f29 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -39,7 +39,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library lewisxhe/PCF8563_Library@1.0.1 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip # TODO renovate diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini index bf9492075..6751dd4ef 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini @@ -25,4 +25,4 @@ lib_deps = # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - ; lewisxhe/SensorLib@0.3.3 + ; lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini index 413eb4fab..a31615545 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini @@ -22,4 +22,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW lib_deps = ${nrf52840_base.lib_deps} ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - ; lewisxhe/SensorLib@0.3.3 + ; lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/nano-g2-ultra/platformio.ini b/variants/nrf52840/nano-g2-ultra/platformio.ini index 0748b7e38..7e593a49a 100644 --- a/variants/nrf52840/nano-g2-ultra/platformio.ini +++ b/variants/nrf52840/nano-g2-ultra/platformio.ini @@ -20,5 +20,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/nano-g2 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 9a66890a7..4acd70b02 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -32,7 +32,7 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs [env:t-echo-inkhud] @@ -53,4 +53,4 @@ lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - lewisxhe/SensorLib@0.3.3 + lewisxhe/SensorLib@0.3.4 From 8894a0b71169f980e06b3131b290483a1bcf5f47 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 25 Jan 2026 18:02:26 -0600 Subject: [PATCH 07/31] Consolidate LoRa params / preset logic and fix display of preset values (#9413) * Consolidate LoRa params / preset logic and fix display of preset values * Move preset coercion logic to RadioInterface * Fix some warnings * Fix warnings * STM32 fix * Add unit tests * Update src/mesh/RadioInterface.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/gps/RTC.cpp | 9 +-- src/gps/RTC.h | 2 +- src/graphics/draw/MenuHandler.cpp | 48 +------------- src/graphics/draw/MenuHandler.h | 2 +- src/mesh/MeshRadio.h | 97 ++++++++++++++++++++++++++++- src/mesh/NodeDB.cpp | 8 +++ src/mesh/RadioInterface.cpp | 92 +++++++++------------------ src/mesh/RadioInterface.h | 9 +++ test/test_radio/test_main.cpp | 100 ++++++++++++++++++++++++++++++ 9 files changed, 252 insertions(+), 115 deletions(-) create mode 100644 test/test_radio/test_main.cpp diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 25cd3ceff..ad26b55a4 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -397,7 +397,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } -time_t gm_mktime(struct tm *tm) +time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ time_t result = 0; @@ -413,8 +413,8 @@ time_t gm_mktime(struct tm *tm) days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); // Now, within this tm->year, compute the days *before* this tm->month starts. - int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year - int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 // If this is a leap year, and we're past February, add a day: if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { @@ -435,6 +435,7 @@ time_t gm_mktime(struct tm *tm) return result; #else - return mktime(tm); + struct tm tmCopy = *tm; + return mktime(&tmCopy); #endif } diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 06dd34c16..cf6db0239 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -54,7 +54,7 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); -time_t gm_mktime(struct tm *tm); +time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index c5a4106e7..6d29e9f7f 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -266,52 +266,8 @@ void menuHandler::FrequencySlotPicker() // Calculate number of channels (copied from RadioInterface::applyModemConfig()) meshtastic_Config_LoRaConfig &loraConfig = config.lora; - double bw = loraConfig.bandwidth; - if (loraConfig.use_preset) { - switch (loraConfig.modem_preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - break; - default: - bw = (myRegion->wideLora) ? 812.5 : 250; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - bw = (myRegion->wideLora) ? 406.25 : 125; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - bw = (myRegion->wideLora) ? 406.25 : 125; - break; - } - } else { - bw = loraConfig.bandwidth; - if (bw == 31) // This parameter is not an integer - bw = 31.25; - if (bw == 62) // Fix for 62.5Khz bandwidth - bw = 62.5; - if (bw == 200) - bw = 203.125; - if (bw == 400) - bw = 406.25; - if (bw == 800) - bw = 812.5; - if (bw == 1600) - bw = 1625.0; - } + double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) + : bwCodeToKHz(loraConfig.bandwidth); uint32_t numChannels = 0; if (myRegion) { diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 45fd0bf5f..1b964678b 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -140,7 +140,7 @@ struct ScreenColor { uint8_t b; bool useVariant; - ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) + explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) : r(rIn), g(gIn), b(bIn), useVariant(variantIn) { } diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index f2514eea1..bbb0ee00f 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -22,4 +22,99 @@ struct RegionInfo { extern const RegionInfo regions[]; extern const RegionInfo *myRegion; -extern void initRegion(); \ No newline at end of file +extern void initRegion(); + +static inline float bwCodeToKHz(uint16_t bwCode) +{ + if (bwCode == 31) + return 31.25f; + if (bwCode == 62) + return 62.5f; + if (bwCode == 200) + return 203.125f; + if (bwCode == 400) + return 406.25f; + if (bwCode == 800) + return 812.5f; + if (bwCode == 1600) + return 1625.0f; + return (float)bwCode; +} + +static inline uint16_t bwKHzToCode(float bwKHz) +{ + if (bwKHz > 31.24f && bwKHz < 31.26f) + return 31; + if (bwKHz > 62.49f && bwKHz < 62.51f) + return 62; + if (bwKHz > 203.12f && bwKHz < 203.13f) + return 200; + if (bwKHz > 406.24f && bwKHz < 406.26f) + return 400; + if (bwKHz > 812.49f && bwKHz < 812.51f) + return 800; + if (bwKHz > 1624.99f && bwKHz < 1625.01f) + return 1600; + return (uint16_t)(bwKHz + 0.5f); +} + +static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf, + uint8_t &cr) +{ + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + bwKHz = wideLora ? 1625.0f : 500.0f; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 7; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 9; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 10; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bwKHz = wideLora ? 1625.0f : 500.0f; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + bwKHz = wideLora ? 406.25f : 125.0f; + cr = 8; + sf = 11; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bwKHz = wideLora ? 406.25f : 125.0f; + cr = 8; + sf = 12; + break; + default: // LONG_FAST (or illegal) + bwKHz = wideLora ? 812.5f : 250.0f; + cr = 5; + sf = 11; + break; + } +} + +static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora) +{ + float bwKHz = 0; + uint8_t sf = 0; + uint8_t cr = 0; + modemPresetToParams(preset, wideLora, bwKHz, sf, cr); + return bwKHz; +} \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 375bc76e3..f87fa58b1 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -13,6 +13,7 @@ #include "PacketHistory.h" #include "PowerFSM.h" #include "RTC.h" +#include "RadioInterface.h" #include "Router.h" #include "SPILock.h" #include "SafeFile.h" @@ -1297,6 +1298,13 @@ void NodeDB::loadFromDisk() LOG_INFO("Loaded saved config version %d", config.version); } } + + // Coerce LoRa config fields derived from presets while bootstrapping. + // Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode. + if (config.has_lora && config.lora.use_preset) { + RadioInterface::bootstrapLoRaConfigFromPreset(config.lora); + } + if (backupSecurity.private_key.size > 0) { LOG_DEBUG("Restoring backup of security config"); config.security = backupSecurity; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index aaaca719e..26ef162b9 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -220,6 +220,34 @@ void initRegion() myRegion = r; } +void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig) +{ + if (!loraConfig.use_preset) { + return; + } + + // Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes). + const RegionInfo *r = regions; + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++) + ; + + const bool regionWideLora = r->wideLora; + + float bwKHz = 0; + uint8_t sf = 0; + uint8_t cr = 0; + modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); + + // If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST. + if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) { + loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); + } + + loraConfig.bandwidth = bwKHzToCode(bwKHz); + loraConfig.spread_factor = sf; +} + /** * ## LoRaWAN for North America @@ -474,54 +502,7 @@ void RadioInterface::applyModemConfig() bool validConfig = false; // We need to check for a valid configuration while (!validConfig) { if (loraConfig.use_preset) { - - switch (loraConfig.modem_preset) { - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 7; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 8; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 9; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 10; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: - bw = (myRegion->wideLora) ? 1625.0 : 500; - cr = 8; - sf = 11; - break; - default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. - bw = (myRegion->wideLora) ? 812.5 : 250; - cr = 5; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 11; - break; - case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: - bw = (myRegion->wideLora) ? 406.25 : 125; - cr = 8; - sf = 12; - break; - } + modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr); if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { cr = loraConfig.coding_rate; LOG_INFO("Using custom Coding Rate %u", cr); @@ -529,20 +510,7 @@ void RadioInterface::applyModemConfig() } else { sf = loraConfig.spread_factor; cr = loraConfig.coding_rate; - bw = loraConfig.bandwidth; - - if (bw == 31) // This parameter is not an integer - bw = 31.25; - if (bw == 62) // Fix for 62.5Khz bandwidth - bw = 62.5; - if (bw == 200) - bw = 203.125; - if (bw == 400) - bw = 406.25; - if (bw == 800) - bw = 812.5; - if (bw == 1600) - bw = 1625.0; + bw = bwCodeToKHz(loraConfig.bandwidth); } if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 6049a11cc..e4dc02de5 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -7,6 +7,9 @@ #include "airtime.h" #include "error.h" +// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. +typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; + #define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission #define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx @@ -115,6 +118,12 @@ class RadioInterface virtual ~RadioInterface() {} + /** + * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. + * This is used during early bootstrapping so UIs that display these fields directly remain consistent. + */ + static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); + /** * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) * diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp new file mode 100644 index 000000000..fbe2b1b13 --- /dev/null +++ b/test/test_radio/test_main.cpp @@ -0,0 +1,100 @@ +#include "MeshRadio.h" +#include "RadioInterface.h" +#include "TestUtil.h" +#include + +#include "meshtastic/config.pb.h" + +static void test_bwCodeToKHz_specialMappings() +{ + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 62.5f, bwCodeToKHz(62)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 203.125f, bwCodeToKHz(200)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 406.25f, bwCodeToKHz(400)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 812.5f, bwCodeToKHz(800)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 1625.0f, bwCodeToKHz(1600)); +} + +static void test_bwCodeToKHz_passthrough() +{ + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 125.0f, bwCodeToKHz(125)); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); +} + +static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = false; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + cfg.bandwidth = 123; + cfg.spread_factor = 8; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); +} + +static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); +} + +static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + + RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); + TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); + TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor); +} + +void setUp(void) {} +void tearDown(void) {} + +void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_bwCodeToKHz_specialMappings); + RUN_TEST(test_bwCodeToKHz_passthrough); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion); + RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan); + exit(UNITY_END()); +} + +void loop() {} From 8a9830282ab48ab52bbf68e46a4af52161535120 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 25 Jan 2026 20:54:17 -0600 Subject: [PATCH 08/31] Move Lora Init code into LoraInit.cpp/h (#9435) * Move Lora Init code into LoraInit.cpp/h * Add missed extern definition * More carefully check for nullptr in LoraInit.cpp * STM32 fixes * Add extern SPI1 for HW_SPI1_DEVICE * Move initLora to RadioInterface.h --- src/main.cpp | 279 +--------------------------------- src/mesh/RadioInterface.cpp | 294 ++++++++++++++++++++++++++++++++++++ src/mesh/RadioInterface.h | 2 + src/mesh/Router.cpp | 1 - 4 files changed, 302 insertions(+), 274 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ea114ea34..c063aaa6a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,29 +77,10 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #include "mqtt/MQTT.h" #endif -#include "LLCC68Interface.h" -#include "LR1110Interface.h" -#include "LR1120Interface.h" -#include "LR1121Interface.h" -#include "RF95Interface.h" -#include "SX1262Interface.h" -#include "SX1268Interface.h" -#include "SX1280Interface.h" -#include "detect/LoRaRadioType.h" - -#ifdef ARCH_STM32WL -#include "STM32WLE5JCInterface.h" -#endif - -#if defined(ARCH_PORTDUINO) -#include "platform/portduino/SimRadio.h" -#endif - #ifdef ARCH_PORTDUINO #include "linux/LinuxHardwareI2C.h" #include "mesh/raspihttp/PiWebServer.h" #include "platform/portduino/PortduinoGlue.h" -#include "platform/portduino/USBHal.h" #include #include #include @@ -254,9 +235,6 @@ ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; Adafruit_DRV2605 drv; #endif -// Global LoRa radio type -LoRaRadioType radioType = NO_RADIO; - bool isVibrating = false; bool eink_found = true; @@ -293,6 +271,7 @@ const char *getDeviceName() return name; } +// TODO remove from main.cpp static int32_t ledBlinker() { // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if @@ -388,6 +367,7 @@ void setup() // boot sequence will follow when battery level raises to safe mode waitUntilPowerLevelSafe(); + // TODO remove all device-specific setup code to variant.cpp #if defined(R1_NEO) pinMode(DCDC_EN_HOLD, OUTPUT); digitalWrite(DCDC_EN_HOLD, HIGH); @@ -489,11 +469,6 @@ void setup() #endif concurrency::hasBeenSetup = true; -#if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); -#else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); -#endif meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; @@ -632,6 +607,7 @@ void setup() OSThread::setup(); + // TODO make this ifdef based on defined pins and move from main.cpp #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) // The ThinkNodes have their own blink logic // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); @@ -1009,6 +985,7 @@ void setup() } #endif // HAS_SCREEN + // TODO Remove magic string // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string @@ -1304,252 +1281,7 @@ void setup() #endif #endif -#ifdef ARCH_PORTDUINO - // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, - RADIOLIB_PIN_TYPE busy) { - switch (portduino_config.lora_module) { - case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); - case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); - case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); - case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); - case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); - case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); - case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); - case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); - case use_simradio: - return (RadioInterface *)new SimRadio; - default: - assert(0); // shouldn't happen - return (RadioInterface *)nullptr; - } - }; - - LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), - portduino_config.lora_spi_dev.c_str()); - if (portduino_config.lora_spi_dev == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); - } - rIf = - loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, - portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); - - if (!rIf->init()) { - LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); - } - -#elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); -#else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); -#endif - - // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) -#if defined(USE_STM32WLx) - if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("STM32WL init success"); - radioType = STM32WLx_RADIO; - } - } -#endif - -#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); - if (!rIf->init()) { - LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("RF95 init success"); - radioType = RF95_RADIO; - } - } -#endif - -#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); -#ifdef SX126X_DIO3_TCXO_VOLTAGE - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); -#endif - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success"); - rIf = sxIf; - radioType = SX1262_RADIO; - } - } -#endif - -#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1262_RADIO; - } - } - - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); - radioType = SX1262_RADIO; - } - } -#endif - -#if defined(USE_SX1268) -#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); - if (!sxIf->init()) { - LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; - radioType = SX1268_RADIO; - } - } -#endif - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1268 init success"); - radioType = SX1268_RADIO; - } - } -#endif - -#if defined(USE_LLCC68) - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); - if (!rIf->init()) { - LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LLCC68 init success"); - radioType = LLCC68_RADIO; - } - } -#endif - -#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1110 init success"); - radioType = LR1110_RADIO; - } - } -#endif - -#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1120 init success"); - radioType = LR1120_RADIO; - } - } -#endif - -#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 - if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); - if (!rIf->init()) { - LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("LR1121 init success"); - radioType = LR1121_RADIO; - } - } -#endif - -#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 - if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); - if (!rIf->init()) { - LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("SX1280 init success"); - radioType = SX1280_RADIO; - } - } -#endif - - // check if the radio chip matches the selected region - if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { - LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); - config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; - nodeDB->saveToDisk(SEGMENT_CONFIG); - - if (!rIf->reconfigure()) { - LOG_WARN("Reconfigure failed, rebooting"); - if (screen) { - screen->showSimpleBanner("Rebooting..."); - } - rebootAtMsec = millis() + 5000; - } - } + initLoRa(); lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) @@ -1638,6 +1370,7 @@ bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for // This will suppress the current delay and instead try to run ASAP. bool runASAP; +// TODO find better home than main.cpp extern meshtastic_DeviceMetadata getDeviceMetadata() { meshtastic_DeviceMetadata deviceMetadata; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d0097a46a..b489f7939 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -1,17 +1,36 @@ #include "RadioInterface.h" #include "Channels.h" #include "DisplayFormatters.h" +#include "LLCC68Interface.h" +#include "LR1110Interface.h" +#include "LR1120Interface.h" +#include "LR1121Interface.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" +#include "RF95Interface.h" #include "Router.h" +#include "SX1262Interface.h" +#include "SX1268Interface.h" +#include "SX1280Interface.h" #include "configuration.h" +#include "detect/LoRaRadioType.h" #include "main.h" #include "sleep.h" #include #include #include +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#include "platform/portduino/SimRadio.h" +#include "platform/portduino/USBHal.h" +#endif + +#ifdef ARCH_STM32WL> +#include "STM32WLE5JCInterface.h" +#endif + // Calculate 2^n without calling pow() uint32_t pow_of_2(uint32_t n) { @@ -205,6 +224,281 @@ bool RadioInterface::uses_default_frequency_slot = true; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; +// Global LoRa radio type +LoRaRadioType radioType = NO_RADIO; + +extern RadioInterface *rIf; +extern RadioLibHal *RadioLibHAL; +#if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) +extern SPIClass SPI1; +#endif + +bool initLoRa() +{ + if (rIf != nullptr) { + delete rIf; + rIf = nullptr; + } + +#if ARCH_PORTDUINO + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); +#else + SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); +#endif + +#ifdef ARCH_PORTDUINO + // as one can't use a function pointer to the class constructor: + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { + case use_rf95: + return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + case use_sx1262: + return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + case use_sx1268: + return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + case use_sx1280: + return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + case use_lr1110: + return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + case use_lr1120: + return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + case use_lr1121: + return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + case use_llcc68: + return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; + default: + assert(0); // shouldn't happen + return (RadioInterface *)nullptr; + } + }; + + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + if (RadioLibHAL != nullptr) { + delete RadioLibHAL; + RadioLibHAL = nullptr; + } + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + } + rIf = + loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); + + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } + +#elif defined(HW_SPI1_DEVICE) + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); +#else // HW_SPI1_DEVICE + LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); +#endif + +// radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) +#if defined(USE_STM32WLx) + if (!rIf) { + rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No STM32WL radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("STM32WL init success"); + radioType = STM32WLx_RADIO; + } + } +#endif + +#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + if (!rIf->init()) { + LOG_WARN("No RF95 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("RF95 init success"); + radioType = RF95_RADIO; + } + } +#endif + +#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); +#ifdef SX126X_DIO3_TCXO_VOLTAGE + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); +#endif + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio"); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success"); + rIf = sxIf; + radioType = SX1262_RADIO; + } + } +#endif + +#if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1262_RADIO; + } + } + + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead + rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); + radioType = SX1262_RADIO; + } + } +#endif + +#if defined(USE_SX1268) +#if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + // try using the specified TCXO voltage + auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); + if (!sxIf->init()) { + LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + delete sxIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); + rIf = sxIf; + radioType = SX1268_RADIO; + } + } +#endif + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1268 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1268 init success"); + radioType = SX1268_RADIO; + } + } +#endif + +#if defined(USE_LLCC68) + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + if (!rIf->init()) { + LOG_WARN("No LLCC68 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LLCC68 init success"); + radioType = LLCC68_RADIO; + } + } +#endif + +#if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { + rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1110 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1110 init success"); + radioType = LR1110_RADIO; + } + } +#endif + +#if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if (!rIf) { + rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1120 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1120 init success"); + radioType = LR1120_RADIO; + } + } +#endif + +#if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 + if (!rIf) { + rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + if (!rIf->init()) { + LOG_WARN("No LR1121 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("LR1121 init success"); + radioType = LR1121_RADIO; + } + } +#endif + +#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 + if (!rIf) { + rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + if (!rIf->init()) { + LOG_WARN("No SX1280 radio"); + delete rIf; + rIf = NULL; + } else { + LOG_INFO("SX1280 init success"); + radioType = SX1280_RADIO; + } + } +#endif + + // check if the radio chip matches the selected region + if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) { + LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + nodeDB->saveToDisk(SEGMENT_CONFIG); + + if (rIf && !rIf->reconfigure()) { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 5000; + } + } + return rIf != nullptr; +} + void initRegion() { const RegionInfo *r = regions; diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 6049a11cc..ddacf2c5d 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -270,5 +270,7 @@ class RadioInterface } }; +bool initLoRa(); + /// Debug printing for packets void printPacket(const char *prefix, const meshtastic_MeshPacket *p); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index a3861521a..1a3270040 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -7,7 +7,6 @@ #include "RTC.h" #include "configuration.h" -#include "detect/LoRaRadioType.h" #include "main.h" #include "mesh-pb-constants.h" #include "meshUtils.h" From c038cfe69a17d5d4ab9aa22a5437a378b01063e1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 26 Jan 2026 11:54:14 -0600 Subject: [PATCH 09/31] Move device code from main.cpp to earlyInitVariant (#9438) --- src/input/TrackballInterruptBase.cpp | 6 +- src/main.cpp | 80 ++----------------- src/main.h | 12 +-- src/platform/extra_variants/README.md | 2 +- src/platform/nrf52/main-nrf52.cpp | 60 +------------- .../ELECROW-ThinkNode-M5/platformio.ini | 4 + .../esp32s3/ELECROW-ThinkNode-M5/variant.cpp | 12 +++ .../esp32s3/ELECROW-ThinkNode-M5/variant.h | 2 + .../hackaday-communicator/platformio.ini | 6 +- .../esp32s3/hackaday-communicator/variant.cpp | 6 ++ variants/esp32s3/t-deck-pro/platformio.ini | 4 + variants/esp32s3/t-deck-pro/variant.cpp | 14 ++++ variants/esp32s3/t-deck/platformio.ini | 4 + variants/esp32s3/t-deck/variant.cpp | 23 ++++++ variants/esp32s3/tlora-pager/platformio.ini | 4 + variants/esp32s3/tlora-pager/variant.cpp | 31 +++++++ .../nrf52840/ELECROW-ThinkNode-M1/variant.cpp | 18 +++++ .../heltec_mesh_node_t114-inkhud/variant.cpp | 8 ++ .../heltec_mesh_node_t114/variant.cpp | 8 ++ .../nrf52840/heltec_mesh_node_t114/variant.h | 2 +- .../nrf52840/heltec_mesh_solar/variant.cpp | 8 ++ variants/nrf52840/meshlink/variant.cpp | 7 ++ variants/nrf52840/r1-neo/variant.cpp | 8 ++ variants/nrf52840/rak_wismeshtap/variant.cpp | 17 ++++ variants/nrf52840/t-echo/variant.cpp | 10 +++ 25 files changed, 208 insertions(+), 148 deletions(-) create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp create mode 100644 variants/esp32s3/hackaday-communicator/variant.cpp create mode 100644 variants/esp32s3/t-deck-pro/variant.cpp create mode 100644 variants/esp32s3/t-deck/variant.cpp create mode 100644 variants/esp32s3/tlora-pager/variant.cpp diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index bbd07e199..bce62caaf 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -61,11 +61,7 @@ int32_t TrackballInterruptBase::runOnce() uint32_t pressDuration = millis() - pressStartTime; bool buttonStillPressed = false; -#if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); -#else buttonStillPressed = !digitalRead(_pinPress); -#endif if (!buttonStillPressed) { // Button released @@ -135,7 +131,7 @@ int32_t TrackballInterruptBase::runOnce() } #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { + if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) { // Start long press detection pressDetected = true; pressStartTime = millis(); diff --git a/src/main.cpp b/src/main.cpp index c063aaa6a..93cc89296 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,10 +43,6 @@ #include "MessageStore.h" #endif -#ifdef ELECROW_ThinkNode_M5 -PCA9557 io(0x18, &Wire); -#endif - #ifdef ARCH_ESP32 #include "freertosinc.h" #if !MESHTASTIC_EXCLUDE_WEBSERVER @@ -312,6 +308,9 @@ __attribute__((weak, noinline)) bool loopCanSleep() void lateInitVariant() __attribute__((weak)); void lateInitVariant() {} +void earlyInitVariant() __attribute__((weak)); +void earlyInitVariant() {} + // NRF52 (and probably other platforms) can report when system is in power failure mode // (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc). // For example NRF52 will prevent any flash writes in that case automatically @@ -367,27 +366,14 @@ void setup() // boot sequence will follow when battery level raises to safe mode waitUntilPowerLevelSafe(); - // TODO remove all device-specific setup code to variant.cpp -#if defined(R1_NEO) - pinMode(DCDC_EN_HOLD, OUTPUT); - digitalWrite(DCDC_EN_HOLD, HIGH); - pinMode(NRF_ON, OUTPUT); - digitalWrite(NRF_ON, HIGH); -#endif + // Defined in variant.cpp for early init code + earlyInitVariant(); #if defined(PIN_POWER_EN) pinMode(PIN_POWER_EN, OUTPUT); digitalWrite(PIN_POWER_EN, HIGH); #endif -#if defined(ELECROW_ThinkNode_M5) - Wire.begin(48, 47); - io.pinMode(PCA_PIN_EINK_EN, OUTPUT); - io.pinMode(PCA_PIN_POWER_EN, OUTPUT); - io.digitalWrite(PCA_PIN_POWER_EN, HIGH); - // io.pinMode(C2_PIN, OUTPUT); -#endif - #ifdef LED_POWER pinMode(LED_POWER, OUTPUT); digitalWrite(LED_POWER, LED_STATE_ON); @@ -412,62 +398,6 @@ void setup() #endif #endif -#if defined(T_DECK) - // GPIO10 manages all peripheral power supplies - // Turn on peripheral power immediately after MUC starts. - // If some boards are turned on late, ESP32 will reset due to low voltage. - // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , - // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) - pinMode(KB_POWERON, OUTPUT); - digitalWrite(KB_POWERON, HIGH); - // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus - // We need to initialize all CS pins in advance otherwise there will be SPI communication issues - // e.g. when detecting the SD card - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - delay(100); -#elif defined(T_DECK_PRO) - pinMode(LORA_EN, OUTPUT); - digitalWrite(LORA_EN, HIGH); - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(PIN_EINK_CS, OUTPUT); - digitalWrite(PIN_EINK_CS, HIGH); -#elif defined(T_LORA_PAGER) - pinMode(LORA_CS, OUTPUT); - digitalWrite(LORA_CS, HIGH); - pinMode(SDCARD_CS, OUTPUT); - digitalWrite(SDCARD_CS, HIGH); - pinMode(TFT_CS, OUTPUT); - digitalWrite(TFT_CS, HIGH); - pinMode(KB_INT, INPUT_PULLUP); - // io expander - io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); - io.pinMode(EXPANDS_DRV_EN, OUTPUT); - io.digitalWrite(EXPANDS_DRV_EN, HIGH); - io.pinMode(EXPANDS_AMP_EN, OUTPUT); - io.digitalWrite(EXPANDS_AMP_EN, LOW); - io.pinMode(EXPANDS_LORA_EN, OUTPUT); - io.digitalWrite(EXPANDS_LORA_EN, HIGH); - io.pinMode(EXPANDS_GPS_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPS_EN, HIGH); - io.pinMode(EXPANDS_KB_EN, OUTPUT); - io.digitalWrite(EXPANDS_KB_EN, HIGH); - io.pinMode(EXPANDS_SD_EN, OUTPUT); - io.digitalWrite(EXPANDS_SD_EN, HIGH); - io.pinMode(EXPANDS_GPIO_EN, OUTPUT); - io.digitalWrite(EXPANDS_GPIO_EN, HIGH); - io.pinMode(EXPANDS_SD_PULLEN, INPUT); -#elif defined(HACKADAY_COMMUNICATOR) - pinMode(KB_INT, INPUT); -#endif - concurrency::hasBeenSetup = true; meshtastic_Config_DisplayConfig_OledType screen_model = diff --git a/src/main.h b/src/main.h index c3528a63d..91e27951f 100644 --- a/src/main.h +++ b/src/main.h @@ -26,8 +26,8 @@ extern NRF52Bluetooth *nrf52Bluetooth; #if ARCH_PORTDUINO extern HardwareSPI *DisplaySPI; extern HardwareSPI *LoraSPI; - #endif + extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; @@ -47,16 +47,16 @@ extern bool isUSBPowered; extern Adafruit_DRV2605 drv; #endif +#ifdef HAS_PCA9557 +#include +extern PCA9557 io; +#endif + #ifdef HAS_I2S #include "AudioThread.h" extern AudioThread *audioThread; #endif -#ifdef ELECROW_ThinkNode_M5 -#include -extern PCA9557 io; -#endif - #ifdef HAS_UDP_MULTICAST #include "mesh/udp/UdpMulticastHandler.h" extern UdpMulticastHandler *udpHandler; diff --git a/src/platform/extra_variants/README.md b/src/platform/extra_variants/README.md index e558502f0..838014c4f 100644 --- a/src/platform/extra_variants/README.md +++ b/src/platform/extra_variants/README.md @@ -5,7 +5,7 @@ This directory tree is designed to solve two problems. - The ESP32 arduino/platformio project doesn't support the nice "if initVariant() is found, call that after init" behavior of the nrf52 builds (they use initVariant() internally). - Over the years a lot of 'board specific' init code has been added to init() in main.cpp. It would be great to have a general/clean mechanism to allow developers to specify board specific/unique code in a clean fashion without mucking in main. -So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define lateInitVariant() if your board needs it. +So we are borrowing the initVariant() ideas here (by using weak gcc references). You can now define earlyInitVariant() and lateInitVariant() if your board needs them. earlyInitVariant() runs at the beginning of setup() directly after waitUntilPowerLevelSafe(); while lateInitVariant() runs after the LoRa radio is initialized. If you'd like a board specific variant to be run, add the variant.cpp file to an appropriately named subdirectory and check for \_VARIANT_boardname in the cpp file (so that your code is only built for your board). diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 2068fe2a7..11b05165c 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -430,15 +430,6 @@ void cpuDeepSleep(uint32_t msecToWake) Serial1.end(); #endif -#ifdef TTGO_T_ECHO - // To power off the T-Echo, the display must be set - // as an input pin; otherwise, there will be leakage current. - pinMode(PIN_EINK_CS, INPUT); - pinMode(PIN_EINK_DC, INPUT); - pinMode(PIN_EINK_RES, INPUT); - pinMode(PIN_EINK_BUSY, INPUT); -#endif - setBluetoothEnable(false); #ifdef RAK4630 @@ -449,57 +440,8 @@ void cpuDeepSleep(uint32_t msecToWake) // RAK-12039 set pin for Air quality sensor digitalWrite(AQ_SET_PIN, LOW); #endif -#ifdef RAK14014 - // GPIO restores input status, otherwise there will be leakage current - nrf_gpio_cfg_default(TFT_BL); - nrf_gpio_cfg_default(TFT_DC); - nrf_gpio_cfg_default(TFT_CS); - nrf_gpio_cfg_default(TFT_SCLK); - nrf_gpio_cfg_default(TFT_MOSI); - nrf_gpio_cfg_default(TFT_MISO); - nrf_gpio_cfg_default(SCREEN_TOUCH_INT); - nrf_gpio_cfg_default(WB_I2C1_SCL); - nrf_gpio_cfg_default(WB_I2C1_SDA); - - // nrf_gpio_cfg_default(WB_I2C2_SCL); - // nrf_gpio_cfg_default(WB_I2C2_SDA); -#endif -#endif -#ifdef MESHLINK -#ifdef PIN_WD_EN - digitalWrite(PIN_WD_EN, LOW); -#endif -#endif - -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) - nrf_gpio_cfg_default(PIN_GPS_PPS); - detachInterrupt(PIN_GPS_PPS); - detachInterrupt(PIN_BUTTON1); -#endif - -#ifdef ELECROW_ThinkNode_M1 - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - pinMode(pin, OUTPUT); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - digitalWrite(pin, LOW); - } - for (int pin = 0; pin < 48; pin++) { - if (pin == 17 || pin == 19 || pin == 20 || pin == 22 || pin == 23 || pin == 24 || pin == 25 || pin == 9 || pin == 10 || - pin == PIN_BUTTON1 || pin == PIN_BUTTON2) { - continue; - } - NRF_GPIO->DIRCLR = (1 << pin); - } #endif + // Run shutdown code if specified in variant.cpp variant_shutdown(); // Sleepy trackers or sensors can low power "sleep" diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index 9994cf665..ee51018d4 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -11,6 +11,10 @@ custom_meshtastic_requires_dfu = false extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/ELECROW-ThinkNode-M5> + build_flags = ${esp32s3_base.build_flags} -D ELECROW_ThinkNode_M5 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp new file mode 100644 index 000000000..4b485a1a3 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp @@ -0,0 +1,12 @@ +#include "variant.h" +#include + +PCA9557 io(0x18, &Wire); + +void earlyInitVariant() +{ + Wire.begin(48, 47); + io.pinMode(PCA_PIN_EINK_EN, OUTPUT); + io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); +} diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 5f5133e61..77a64f717 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -4,6 +4,8 @@ #define UART_TX 43 #define UART_RX 44 +#define HAS_PCA9557 + // LED // Both of these are on the GPIO expander #define PCA_LED_USER 1 // the Blue LED diff --git a/variants/esp32s3/hackaday-communicator/platformio.ini b/variants/esp32s3/hackaday-communicator/platformio.ini index 29b2c2305..8fd275c0e 100644 --- a/variants/esp32s3/hackaday-communicator/platformio.ini +++ b/variants/esp32s3/hackaday-communicator/platformio.ini @@ -6,6 +6,10 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/hackaday-communicator> + build_flags = ${esp32s3_base.build_flags} -D HACKADAY_COMMUNICATOR -D BOARD_HAS_PSRAM @@ -13,4 +17,4 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-Arduino_GFX packageName=https://github.com/meshtastic/Arduino_GFX gitBranch=master - https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip + https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip \ No newline at end of file diff --git a/variants/esp32s3/hackaday-communicator/variant.cpp b/variants/esp32s3/hackaday-communicator/variant.cpp new file mode 100644 index 000000000..d85b2abb5 --- /dev/null +++ b/variants/esp32s3/hackaday-communicator/variant.cpp @@ -0,0 +1,6 @@ +#include "variant.h" +#include "Arduino.h" +void earlyInitVariant() +{ + pinMode(KB_INT, INPUT); +} \ No newline at end of file diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index aca31b599..5ba82d045 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -15,6 +15,10 @@ board = t-deck-pro board_check = true upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t-deck-pro> + build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/t-deck-pro -D T_DECK_PRO diff --git a/variants/esp32s3/t-deck-pro/variant.cpp b/variants/esp32s3/t-deck-pro/variant.cpp new file mode 100644 index 000000000..509726c52 --- /dev/null +++ b/variants/esp32s3/t-deck-pro/variant.cpp @@ -0,0 +1,14 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(LORA_EN, OUTPUT); + digitalWrite(LORA_EN, HIGH); + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(PIN_EINK_CS, OUTPUT); + digitalWrite(PIN_EINK_CS, HIGH); +} \ No newline at end of file diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 58335796a..54ffe43fe 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -17,6 +17,10 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t-deck> + build_flags = ${esp32s3_base.build_flags} -D T_DECK -D BOARD_HAS_PSRAM diff --git a/variants/esp32s3/t-deck/variant.cpp b/variants/esp32s3/t-deck/variant.cpp new file mode 100644 index 000000000..6b68f142c --- /dev/null +++ b/variants/esp32s3/t-deck/variant.cpp @@ -0,0 +1,23 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + // GPIO10 manages all peripheral power supplies + // Turn on peripheral power immediately after MUC starts. + // If some boards are turned on late, ESP32 will reset due to low voltage. + // ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) , + // TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder) + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus + // We need to initialize all CS pins in advance otherwise there will be SPI communication issues + // e.g. when detecting the SD card + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + delay(100); +} \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index ea6369f29..7b4fc5312 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -17,6 +17,10 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/tlora-pager> + build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tlora-pager -D T_LORA_PAGER diff --git a/variants/esp32s3/tlora-pager/variant.cpp b/variants/esp32s3/tlora-pager/variant.cpp new file mode 100644 index 000000000..7b0cbdfec --- /dev/null +++ b/variants/esp32s3/tlora-pager/variant.cpp @@ -0,0 +1,31 @@ +#include "variant.h" +#include "ExtensionIOXL9555.hpp" +extern ExtensionIOXL9555 io; + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + pinMode(KB_INT, INPUT_PULLUP); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); +} \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index cae079b74..2ce84bc57 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -42,3 +42,21 @@ void initVariant() pinMode(PIN_LED3, OUTPUT); ledOff(PIN_LED3); } + +void variant_shutdown() +{ + for (int pin = 0; pin < 48; pin++) { + if (pin == SX126X_BUSY || pin == PIN_SPI_SCK || pin == SX126X_DIO1 || pin == PIN_SPI_MOSI || pin == PIN_SPI_MISO || + pin == SX126X_CS || pin == SX126X_RESET || pin == PIN_NFC1 || pin == PIN_NFC2 || pin == PIN_BUTTON1 || + pin == PIN_BUTTON2) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp index 85c9f4a72..b8b0e21c5 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -36,3 +37,10 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp index 85c9f4a72..b8b0e21c5 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.cpp +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -36,3 +37,10 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index fb7f61ac7..cf14b5e04 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -156,7 +156,7 @@ No longer populated on PCB // The bluetooth transmit power on the nRF52840 is adjustable from -20dB to +8dB in steps of 4dB // so NRF52_BLE_TX_POWER can be set to -20, -16, -12, -8, -4, 0 (default), 4, and 8. -//#define NRF52_BLE_TX_POWER 8 +// #define NRF52_BLE_TX_POWER 8 /* * GPS pins diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp index c13f006d7..3b2612da8 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.cpp +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -19,6 +19,7 @@ */ #include "variant.h" +#include "Arduino.h" #include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -38,3 +39,10 @@ void initVariant() digitalWrite(PIN_SCREEN_VDD_CTL, LOW); // Start with power on #endif } + +void variant_shutdown() +{ + nrf_gpio_cfg_default(PIN_GPS_PPS); + detachInterrupt(PIN_GPS_PPS); + detachInterrupt(PIN_BUTTON1); +} \ No newline at end of file diff --git a/variants/nrf52840/meshlink/variant.cpp b/variants/nrf52840/meshlink/variant.cpp index 81a5097c4..f35bbd1af 100644 --- a/variants/nrf52840/meshlink/variant.cpp +++ b/variants/nrf52840/meshlink/variant.cpp @@ -20,4 +20,11 @@ void initVariant() pinMode(PIN_WD_EN, OUTPUT); digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot #endif +} + +void variant_shutdown() +{ +#ifdef PIN_WD_EN + digitalWrite(PIN_WD_EN, LOW); +#endif } \ No newline at end of file diff --git a/variants/nrf52840/r1-neo/variant.cpp b/variants/nrf52840/r1-neo/variant.cpp index f87c041aa..d87b88c85 100644 --- a/variants/nrf52840/r1-neo/variant.cpp +++ b/variants/nrf52840/r1-neo/variant.cpp @@ -43,3 +43,11 @@ void initVariant() // pinMode(PIN_3V3_EN, OUTPUT); // digitalWrite(PIN_3V3_EN, HIGH); } + +void earlyInitVariant() +{ + pinMode(DCDC_EN_HOLD, OUTPUT); + digitalWrite(DCDC_EN_HOLD, HIGH); + pinMode(NRF_ON, OUTPUT); + digitalWrite(NRF_ON, HIGH); +} diff --git a/variants/nrf52840/rak_wismeshtap/variant.cpp b/variants/nrf52840/rak_wismeshtap/variant.cpp index 5a3587982..36572b074 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.cpp +++ b/variants/nrf52840/rak_wismeshtap/variant.cpp @@ -42,4 +42,21 @@ void initVariant() // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); +} + +void variant_shutdown() +{ + // GPIO restores input status, otherwise there will be leakage current + nrf_gpio_cfg_default(TFT_BL); + nrf_gpio_cfg_default(TFT_DC); + nrf_gpio_cfg_default(TFT_CS); + nrf_gpio_cfg_default(TFT_SCLK); + nrf_gpio_cfg_default(TFT_MOSI); + nrf_gpio_cfg_default(TFT_MISO); + nrf_gpio_cfg_default(SCREEN_TOUCH_INT); + nrf_gpio_cfg_default(WB_I2C1_SCL); + nrf_gpio_cfg_default(WB_I2C1_SDA); + + // nrf_gpio_cfg_default(WB_I2C2_SCL); + // nrf_gpio_cfg_default(WB_I2C2_SDA); } \ No newline at end of file diff --git a/variants/nrf52840/t-echo/variant.cpp b/variants/nrf52840/t-echo/variant.cpp index cae079b74..cb64530f6 100644 --- a/variants/nrf52840/t-echo/variant.cpp +++ b/variants/nrf52840/t-echo/variant.cpp @@ -42,3 +42,13 @@ void initVariant() pinMode(PIN_LED3, OUTPUT); ledOff(PIN_LED3); } + +void variant_shutdown() +{ + // To power off the T-Echo, the display must be set + // as an input pin; otherwise, there will be leakage current. + pinMode(PIN_EINK_CS, INPUT); + pinMode(PIN_EINK_DC, INPUT); + pinMode(PIN_EINK_RES, INPUT); + pinMode(PIN_EINK_BUSY, INPUT); +} \ No newline at end of file From 3d58c6e91676f36ee518a3f99d6b825a146091e4 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 26 Jan 2026 14:28:05 -0600 Subject: [PATCH 10/31] Trackball revamp (#9440) * Trackball revamp * Use Throttle * Volatile! --- src/input/TrackballInterruptBase.cpp | 82 ++++++++++++++++++++++----- src/input/TrackballInterruptBase.h | 14 ++++- src/input/TrackballInterruptImpl1.cpp | 35 ++++-------- variants/esp32s3/t-deck/variant.h | 1 + 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index bce62caaf..1bbe75629 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,5 +1,7 @@ #include "TrackballInterruptBase.h" +#include "Throttle.h" #include "configuration.h" + extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} @@ -55,6 +57,20 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; +#if TB_THRESHOLD + if (lastInterruptTime && !Throttle::isWithinTimespanMs(lastInterruptTime, 1000)) { + left_counter = 0; + right_counter = 0; + up_counter = 0; + down_counter = 0; + lastInterruptTime = 0; + } +#ifdef INPUT_DEBUG + if (left_counter > 0 || right_counter > 0 || up_counter > 0 || down_counter > 0) { + LOG_DEBUG("L %u R %u U %u D %u, time %u", left_counter, right_counter, up_counter, down_counter, millis()); + } +#endif +#endif // Handle long press detection for press button if (pressDetected && pressStartTime > 0) { @@ -130,23 +146,31 @@ int32_t TrackballInterruptBase::runOnce() } } -#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball +#if TB_THRESHOLD if (this->action == TB_ACTION_PRESSED && (!pressDetected || pressStartTime == 0)) { // Start long press detection pressDetected = true; pressStartTime = millis(); // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { - // LOG_DEBUG("Trackball event UP"); + } else if (up_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event UP %u", millis()); +#endif e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { - // LOG_DEBUG("Trackball event DOWN"); + } else if (down_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event DOWN %u", millis()); +#endif e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { - // LOG_DEBUG("Trackball event LEFT"); + } else if (left_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event LEFT %u", millis()); +#endif e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { - // LOG_DEBUG("Trackball event RIGHT"); + } else if (right_counter >= TB_THRESHOLD) { +#ifdef INPUT_DEBUG + LOG_DEBUG("Trackball event RIGHT %u", millis()); +#endif e.inputEvent = this->_eventRight; } #else @@ -179,6 +203,12 @@ int32_t TrackballInterruptBase::runOnce() e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); +#if TB_THRESHOLD + left_counter = 0; + right_counter = 0; + up_counter = 0; + down_counter = 0; +#endif } // Only update lastEvent for non-press actions or completed press actions @@ -194,25 +224,49 @@ int32_t TrackballInterruptBase::runOnce() void TrackballInterruptBase::intPressHandler() { - this->action = TB_ACTION_PRESSED; + if (!Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_PRESSED; + lastInterruptTime = millis(); } void TrackballInterruptBase::intDownHandler() { - this->action = TB_ACTION_DOWN; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_DOWN; + lastInterruptTime = millis(); + +#if TB_THRESHOLD + down_counter++; +#endif } void TrackballInterruptBase::intUpHandler() { - this->action = TB_ACTION_UP; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_UP; + lastInterruptTime = millis(); + +#if TB_THRESHOLD + up_counter++; +#endif } void TrackballInterruptBase::intLeftHandler() { - this->action = TB_ACTION_LEFT; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_LEFT; + lastInterruptTime = millis(); +#if TB_THRESHOLD + left_counter++; +#endif } void TrackballInterruptBase::intRightHandler() { - this->action = TB_ACTION_RIGHT; + if (TB_THRESHOLD || !Throttle::isWithinTimespanMs(lastInterruptTime, 10)) + this->action = TB_ACTION_RIGHT; + lastInterruptTime = millis(); +#if TB_THRESHOLD + right_counter++; +#endif } diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 67d4ee449..908f62769 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -12,6 +12,10 @@ #endif #endif +#ifndef TB_THRESHOLD +#define TB_THRESHOLD 0 +#endif + class TrackballInterruptBase : public Observable, public concurrency::OSThread { public: @@ -25,8 +29,6 @@ class TrackballInterruptBase : public Observable, public con void intUpHandler(); void intLeftHandler(); void intRightHandler(); - uint32_t lastTime = 0; - virtual int32_t runOnce() override; protected: @@ -67,4 +69,12 @@ class TrackballInterruptBase : public Observable, public con input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; + volatile uint32_t lastInterruptTime = 0; + +#if TB_THRESHOLD + volatile uint8_t left_counter = 0; + volatile uint8_t right_counter = 0; + volatile uint8_t up_counter = 0; + volatile uint8_t down_counter = 0; +#endif }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..fd126913a 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -24,41 +24,26 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe void TrackballInterruptImpl1::handleIntDown() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intDownHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntUp() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intUpHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntLeft() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intLeftHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntRight() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intRightHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } void TrackballInterruptImpl1::handleIntPressed() { - if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { - trackballInterruptImpl1->lastTime = millis(); - trackballInterruptImpl1->intPressHandler(); - trackballInterruptImpl1->setIntervalFromNow(20); - } + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); } diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index 8d2996131..ab5b74870 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -71,6 +71,7 @@ #define TB_RIGHT 2 #define TB_PRESS 0 // BUTTON_PIN #define TB_DIRECTION FALLING +#define TB_THRESHOLD 3 // microphone #define ES7210_SCK 47 From 7efc3e37708cdaa0762baec12bd178b7760fa938 Mon Sep 17 00:00:00 2001 From: Keane <2617725+k3an3@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:32:03 -0500 Subject: [PATCH 11/31] Replace strcpy with strncpy and null termination (#9436) Co-authored-by: Jonathan Bennett --- src/mqtt/MQTT.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 4c2c0fe1b..18a4f913e 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -475,8 +475,10 @@ bool MQTT::publish(const char *topic, const char *payload, bool retained) if (moduleConfig.mqtt.proxy_to_client_enabled) { meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; - strcpy(msg->topic, topic); - strcpy(msg->payload_variant.text, payload); + strncpy(msg->topic, topic, sizeof(msg->topic)); + msg->topic[sizeof(msg->topic) - 1] = '\0'; + strncpy(msg->payload_variant.text, payload, sizeof(msg->payload_variant.text)); + msg->payload_variant.text[sizeof(msg->payload_variant.text) - 1] = '\0'; msg->retained = retained; service->sendMqttMessageToClientProxy(msg); return true; From a2e8e232f11ee7660a5e92c8fd0d6c72439369d7 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 26 Jan 2026 16:58:16 -0600 Subject: [PATCH 12/31] Remove the unused OCV_ARRAYs and move one to a variant.h (#9442) --- src/power.h | 12 ------------ variants/esp32/chatter2/variant.h | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/power.h b/src/power.h index 5f887c36b..e4b456d3b 100644 --- a/src/power.h +++ b/src/power.h @@ -15,20 +15,8 @@ // Device specific curves go in variant.h #ifndef OCV_ARRAY -#ifdef CELL_TYPE_LIFEPO4 -#define OCV_ARRAY 3400, 3350, 3320, 3290, 3270, 3260, 3250, 3230, 3200, 3120, 3000 -#elif defined(CELL_TYPE_LEADACID) -#define OCV_ARRAY 2120, 2090, 2070, 2050, 2030, 2010, 1990, 1980, 1970, 1960, 1950 -#elif defined(CELL_TYPE_ALKALINE) -#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 -#elif defined(CELL_TYPE_NIMH) -#define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000 -#elif defined(CELL_TYPE_LTO) -#define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 -#else // LiIon #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #endif -#endif /*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/ #ifndef NUM_CELLS diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index 0c1ef6967..abcb1ce4d 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -79,7 +79,7 @@ // lower dB for lower voltage rnage #define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND // Chatter2 uses 3 AAA cells -#define CELL_TYPE_ALKALINE +#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000 #define NUM_CELLS 3 #undef EXT_PWR_DETECT From 63a97a54e110bd1d22f39812229becba0555da68 Mon Sep 17 00:00:00 2001 From: Colby Dillion Date: Mon, 26 Jan 2026 18:45:24 -0600 Subject: [PATCH 13/31] Fix retry_delay calculation for error responses (#9443) Co-authored-by: Ben Meadors --- src/modules/StoreForwardModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index b8a710bf5..023a1c798 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -513,7 +513,7 @@ bool StoreForwardModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, LOG_DEBUG("StoreAndForward_RequestResponse_ROUTER_BUSY"); // retry in messages_saved * packetTimeMax ms retry_delay = millis() + getNumAvailablePackets(this->busyTo, this->last_time) * packetTimeMax * - (meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); + (p->rr == meshtastic_StoreAndForward_RequestResponse_ROUTER_ERROR ? 2 : 1); } break; From 90778a4e788b70442a705f43dd92916b9b028248 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Tue, 27 Jan 2026 20:03:31 +0800 Subject: [PATCH 14/31] feat(GPS): Support Softsleep with WAKE-UP pin on PA1010D (#9078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support softsleep by defining PIN_GPS_STANDBY on CDTop CD-PA1010D. This differs from existing MTK GPS e.g. L76K, where pulling PIN_GPS_STANDBY (WAKE-UP pin) low is not sufficient to put the GPS module in standby. An additional `$PMTK225,4*2F` must be sent to enter "Backup Mode", which is exited by bringing PIN_GPS_STANDBY (WAKE-UP pin) high. Refer to datasheet[0] §1.9.3 "Backup Mode". 0: https://cdn-learn.adafruit.com/assets/assets/000/084/295/original/CD_PA1010D_Datasheet_v.03.pdf Signed-off-by: Andrew Yong --- src/gps/GPS.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index fd121861c..13e5c32d1 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -905,6 +905,12 @@ void GPS::writePinStandby(bool standby) // Write and log pinMode(PIN_GPS_STANDBY, OUTPUT); digitalWrite(PIN_GPS_STANDBY, val); + + // Enter backup mode on PA1010D; TODO: may be applicable to other MTK GPS too + if (IS_ONE_OF(gnssModel, GNSS_MODEL_MTK_PA1010D)) { + _serial_gps->write("$PMTK225,4*2F\r\n"); + } + #ifdef GPS_DEBUG LOG_DEBUG("Pin STANDBY %s", val == HIGH ? "HI" : "LOW"); #endif From c8079d4115abd5e4a3e2902e95e7de9224e0280c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 27 Jan 2026 08:05:36 -0600 Subject: [PATCH 15/31] Metadata for heltec tracker v2 --- .../esp32s3/heltec_wireless_tracker_v2/platformio.ini | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 8bdb71541..0b486618b 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -1,8 +1,17 @@ [env:heltec-wireless-tracker-v2] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_wireless_tracker_v2.svg +custom_meshtastic_tags = Heltec + extends = esp32s3_base board = heltec_wireless_tracker_v2 board_build.partitions = default_8MB.csv upload_protocol = esptool +custom_meshtastic_hw_model = 113 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V2 +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_display_name = Heltec Wireless Tracker V2 +custom_meshtastic_actively_supported = true build_flags = ${esp32s3_base.build_flags} From d54ae5dad8c84eab3b42e2b98d2c15d3c7788256 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:11:11 -0500 Subject: [PATCH 16/31] InkHUD Menu improvements (#8975) * InkHUD: Region Picker on initial setup * Added Node Config menu with Lora Region Picker * Role picker * Preset Picker * Timezone picker added * Power save mode and bluetooth configs * Config section Headers * Channel Config * Cleaning some behavior * Add back to all Options * Display config added * Position Toggle added * Network Config for ESP32 * Wifi details * Reduce line spacing to fit more content * Recent list with checkboxes * Timezone labels easier to understand * Trunk fix * Added "Saving Changes" screen when reboot is needed * Trunk fix * Make Tips show after first boot if the region is Unset * Added ResetDB and keep only favorite commands * quick fix to joystick * Trunk Fix * Fix to tips to work with new joystick input * Added ADC multiplier value display on power config * added ADC calibration feature * Fixed missing stray endiff * GPS toggle now is aware if gps is present. --- .../InkHUD/Applets/System/Logo/LogoApplet.cpp | 12 + .../InkHUD/Applets/System/Logo/LogoApplet.h | 1 + .../InkHUD/Applets/System/Menu/MenuAction.h | 79 ++ .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 1127 ++++++++++++++++- .../InkHUD/Applets/System/Menu/MenuApplet.h | 12 +- .../InkHUD/Applets/System/Menu/MenuItem.h | 7 + .../InkHUD/Applets/System/Menu/MenuPage.h | 19 +- .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 192 ++- .../InkHUD/Applets/System/Tips/TipsApplet.h | 1 + src/graphics/niche/InkHUD/Events.cpp | 9 + src/graphics/niche/InkHUD/Events.h | 13 +- src/graphics/niche/InkHUD/InkHUD.cpp | 7 + src/graphics/niche/InkHUD/InkHUD.h | 4 + src/graphics/niche/InkHUD/SystemApplet.h | 1 + 14 files changed, 1344 insertions(+), 140 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index ecaa7cea3..4b55529bb 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -155,6 +155,18 @@ void InkHUD::LogoApplet::onShutdown() // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete } +void InkHUD::LogoApplet::onApplyingChanges() +{ + bringToForeground(); + + textLeft = ""; + textRight = ""; + textTitle = "Applying changes"; + fontTitle = fontSmall; + + inkhud->forceUpdate(Drivers::EInk::FAST, false); +} + void InkHUD::LogoApplet::onReboot() { bringToForeground(); diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index 3f604baed..37f940453 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -26,6 +26,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread void onBackground() override; void onShutdown() override; void onReboot() override; + void onApplyingChanges(); protected: int32_t runOnce() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index debe2b719..74ad5c85f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -22,6 +22,7 @@ enum MenuAction { STORE_CANNEDMESSAGE_SELECTION, SEND_CANNEDMESSAGE, SHUTDOWN, + BACK, NEXT_TILE, TOGGLE_BACKLIGHT, TOGGLE_GPS, @@ -36,6 +37,84 @@ enum MenuAction { TOGGLE_NOTIFICATIONS, TOGGLE_INVERT_COLOR, TOGGLE_12H_CLOCK, + // Regions + SET_REGION_US, + SET_REGION_EU_868, + SET_REGION_EU_433, + SET_REGION_CN, + SET_REGION_JP, + SET_REGION_ANZ, + SET_REGION_KR, + SET_REGION_TW, + SET_REGION_RU, + SET_REGION_IN, + SET_REGION_NZ_865, + SET_REGION_TH, + SET_REGION_LORA_24, + SET_REGION_UA_433, + SET_REGION_UA_868, + SET_REGION_MY_433, + SET_REGION_MY_919, + SET_REGION_SG_923, + SET_REGION_PH_433, + SET_REGION_PH_868, + SET_REGION_PH_915, + SET_REGION_ANZ_433, + SET_REGION_KZ_433, + SET_REGION_KZ_863, + SET_REGION_NP_865, + SET_REGION_BR_902, + // Device Roles + SET_ROLE_CLIENT, + SET_ROLE_CLIENT_MUTE, + SET_ROLE_ROUTER, + SET_ROLE_REPEATER, + // Presets + SET_PRESET_LONG_SLOW, + SET_PRESET_LONG_MODERATE, + SET_PRESET_LONG_FAST, + SET_PRESET_MEDIUM_SLOW, + SET_PRESET_MEDIUM_FAST, + SET_PRESET_SHORT_SLOW, + SET_PRESET_SHORT_FAST, + SET_PRESET_SHORT_TURBO, + // Timezones + SET_TZ_US_HAWAII, + SET_TZ_US_ALASKA, + SET_TZ_US_PACIFIC, + SET_TZ_US_ARIZONA, + SET_TZ_US_MOUNTAIN, + SET_TZ_US_CENTRAL, + SET_TZ_US_EASTERN, + SET_TZ_BR_BRAZILIA, + SET_TZ_UTC, + SET_TZ_EU_WESTERN, + SET_TZ_EU_CENTRAL, + SET_TZ_EU_EASTERN, + SET_TZ_ASIA_KOLKATA, + SET_TZ_ASIA_HONG_KONG, + SET_TZ_AU_AWST, + SET_TZ_AU_ACST, + SET_TZ_AU_AEST, + SET_TZ_PACIFIC_NZ, + // Power + TOGGLE_POWER_SAVE, + CALIBRATE_ADC, + // Bluetooth + TOGGLE_BLUETOOTH, + TOGGLE_BLUETOOTH_PAIR_MODE, + // Channel + TOGGLE_CHANNEL_UPLINK, + TOGGLE_CHANNEL_DOWNLINK, + TOGGLE_CHANNEL_POSITION, + SET_CHANNEL_PRECISION, + // Display + TOGGLE_DISPLAY_UNITS, + // Network + TOGGLE_WIFI, + // Administration + RESET_NODEDB_ALL, + RESET_NODEDB_KEEP_FAVORITES, }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 7e7093857..93d2c6b83 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -2,16 +2,21 @@ #include "./MenuApplet.h" -#include "RTC.h" - +#include "DisplayFormatters.h" +#include "GPS.h" #include "MeshService.h" +#include "RTC.h" #include "Router.h" #include "airtime.h" #include "main.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" #include "power.h" - -#if !MESHTASTIC_EXCLUDE_GPS -#include "GPS.h" +#include +#include +#if defined(ARCH_ESP32) && HAS_WIFI +#include "mesh/wifi/WiFiAPClient.h" +#include +#include #endif using namespace NicheGraphics; @@ -22,6 +27,18 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu // These are offered to users as possible values for settings.recentlyActiveSeconds static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120}; +struct PositionPrecisionOption { + uint8_t value; // proto value + const char *metric; + const char *imperial; +}; + +static constexpr PositionPrecisionOption POSITION_PRECISION_OPTIONS[] = { + {32, "Precise", "Precise"}, {19, "50 m", "150 ft"}, {18, "90 m", "300 ft"}, {17, "200 m", "600 ft"}, + {16, "350 m", "0.2 mi"}, {15, "700 m", "0.5 mi"}, {14, "1.5 km", "0.9 mi"}, {13, "2.9 km", "1.8 mi"}, + {12, "5.8 km", "3.6 mi"}, {11, "12 km", "7.3 mi"}, {10, "23 km", "15 mi"}, +}; + InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") { // No timer tasks at boot @@ -45,8 +62,15 @@ void InkHUD::MenuApplet::onForeground() // We do need this before we render, but we can optimize by just calculating it once now systemInfoPanelHeight = getSystemInfoPanelHeight(); - // Display initial menu page - showPage(MenuPage::ROOT); + // Force Region page ONLY when explicitly requested (one-shot) + if (inkhud->forceRegionMenu) { + + inkhud->forceRegionMenu = false; // consume one-shot flag + showPage(MenuPage::REGION); + + } else { + showPage(MenuPage::ROOT); + } // If device has a backlight which isn't controlled by aux button: // backlight on always when menu opens. @@ -139,6 +163,150 @@ int32_t InkHUD::MenuApplet::runOnce() return OSThread::disable(); } +static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region) +{ + if (config.lora.region == region) + return; + + config.lora.region = region; + + auto changes = SEGMENT_CONFIG; + +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (!owner.is_licensed) { + bool keygenSuccess = false; + + if (config.security.private_key.size == 32) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } +#endif + + config.lora.tx_enabled = true; + + initRegion(); + + if (myRegion && myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; + } + + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + changes |= SEGMENT_MODULECONFIG; + } + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + service->reloadConfig(changes); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static void applyDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + if (config.device.role == role) + return; + + config.device.role = role; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + + service->reloadConfig(SEGMENT_CONFIG); + + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static void applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + if (config.lora.modem_preset == preset) + return; + + config.lora.use_preset = true; + config.lora.modem_preset = preset; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); + + // Notify UI that changes are being applied + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; +} + +static const char *getTimezoneLabelFromValue(const char *tzdef) +{ + if (!tzdef || !*tzdef) + return "Unset"; + + // Must match TIMEZONE menu entries + if (strcmp(tzdef, "HST10") == 0) + return "US/Hawaii"; + if (strcmp(tzdef, "AKST9AKDT,M3.2.0,M11.1.0") == 0) + return "US/Alaska"; + if (strcmp(tzdef, "PST8PDT,M3.2.0,M11.1.0") == 0) + return "US/Pacific"; + if (strcmp(tzdef, "MST7") == 0) + return "US/Arizona"; + if (strcmp(tzdef, "MST7MDT,M3.2.0,M11.1.0") == 0) + return "US/Mountain"; + if (strcmp(tzdef, "CST6CDT,M3.2.0,M11.1.0") == 0) + return "US/Central"; + if (strcmp(tzdef, "EST5EDT,M3.2.0,M11.1.0") == 0) + return "US/Eastern"; + if (strcmp(tzdef, "BRT3") == 0) + return "BR/Brasilia"; + if (strcmp(tzdef, "UTC0") == 0) + return "UTC"; + if (strcmp(tzdef, "GMT0BST,M3.5.0/1,M10.5.0") == 0) + return "EU/Western"; + if (strcmp(tzdef, "CET-1CEST,M3.5.0,M10.5.0/3") == 0) + return "EU/Central"; + if (strcmp(tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4") == 0) + return "EU/Eastern"; + if (strcmp(tzdef, "IST-5:30") == 0) + return "Asia/Kolkata"; + if (strcmp(tzdef, "HKT-8") == 0) + return "Asia/Hong Kong"; + if (strcmp(tzdef, "AWST-8") == 0) + return "AU/AWST"; + if (strcmp(tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3") == 0) + return "AU/ACST"; + if (strcmp(tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3") == 0) + return "AU/AEST"; + if (strcmp(tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3") == 0) + return "Pacific/NZ"; + + return tzdef; // fallback for unknown/custom values +} + +static void applyTimezone(const char *tz) +{ + if (!tz || strcmp(config.device.tzdef, tz) == 0) + return; + + strncpy(config.device.tzdef, tz, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + + setenv("TZ", config.device.tzdef, 1); + + nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); +} + // Perform action for a menu item, then change page // Behaviors for MenuActions are defined here void InkHUD::MenuApplet::execute(MenuItem item) @@ -150,10 +318,22 @@ void InkHUD::MenuApplet::execute(MenuItem item) // Open a submenu without performing any action // Also handles exit case NO_ACTION: + if (currentPage == MenuPage::NODE_CONFIG_CHANNELS && item.nextPage == MenuPage::NODE_CONFIG_CHANNEL_DETAIL) { + + // cursor - 1 because index 0 is "Back" + selectedChannelIndex = cursor - 1; + } break; + case BACK: + showPage(item.nextPage); + return; + case NEXT_TILE: inkhud->nextTile(); + // Unselect menu item after tile change + cursorShown = false; + cursor = 0; break; case SEND_PING: @@ -196,17 +376,23 @@ void InkHUD::MenuApplet::execute(MenuItem item) break; case TOGGLE_APPLET: - settings->userApplets.active[cursor] = !settings->userApplets.active[cursor]; - inkhud->updateAppletSelection(); + if (item.checkState) { + *item.checkState = !(*item.checkState); + inkhud->updateAppletSelection(); + } break; case TOGGLE_AUTOSHOW_APPLET: // Toggle settings.userApplets.autoshow[] value, via MenuItem::checkState pointer set in populateAutoshowPage() - *items.at(cursor).checkState = !(*items.at(cursor).checkState); + if (item.checkState) { + *item.checkState = !(*item.checkState); + } break; case TOGGLE_NOTIFICATIONS: - settings->optionalFeatures.notifications = !settings->optionalFeatures.notifications; + if (item.checkState) { + *item.checkState = !(*item.checkState); + } break; case TOGGLE_INVERT_COLOR: @@ -218,12 +404,14 @@ void InkHUD::MenuApplet::execute(MenuItem item) nodeDB->saveToDisk(SEGMENT_CONFIG); break; - case SET_RECENTS: - // Set value of settings.recentlyActiveSeconds - // Uses menu cursor to read RECENTS_OPTIONS_MINUTES array (defined at top of this file) - assert(cursor < sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0])); - settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[cursor] * 60; // Menu items are in minutes + case SET_RECENTS: { + // cursor - 1 because index 0 is "Back" + const uint8_t index = cursor - 1; + constexpr uint8_t optionCount = sizeof(RECENTS_OPTIONS_MINUTES) / sizeof(RECENTS_OPTIONS_MINUTES[0]); + assert(index < optionCount); + settings->recentlyActiveSeconds = RECENTS_OPTIONS_MINUTES[index] * 60; break; + } case SHUTDOWN: LOG_INFO("Shutting down from menu"); @@ -251,8 +439,18 @@ void InkHUD::MenuApplet::execute(MenuItem item) break; case TOGGLE_GPS: - gps->toggleGpsMode(); +#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + } else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + } else { + // NOT_PRESENT do nothing + break; + } nodeDB->saveToDisk(SEGMENT_CONFIG); + service->reloadConfig(SEGMENT_CONFIG); +#endif break; case ENABLE_BLUETOOTH: @@ -260,10 +458,397 @@ void InkHUD::MenuApplet::execute(MenuItem item) LOG_INFO("Enabling Bluetooth"); config.network.wifi_enabled = false; config.bluetooth.enabled = true; - nodeDB->saveToDisk(); + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); rebootAtMsec = millis() + 2000; break; + // Power / Network (ESP32-only) +#if defined(ARCH_ESP32) + case TOGGLE_POWER_SAVE: + config.power.is_power_saving = !config.power.is_power_saving; + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case TOGGLE_WIFI: + config.network.wifi_enabled = !config.network.wifi_enabled; + + if (config.network.wifi_enabled) { + // Switch behavior: WiFi ON forces Bluetooth OFF + config.bluetooth.enabled = false; + } + + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; +#endif + // ADC Calibration + case CALIBRATE_ADC: { + // Read current measured voltage + float measuredV = powerStatus->getBatteryVoltageMv() / 1000.0f; + + // Sanity check + if (measuredV < 3.0f || measuredV > 4.5f) { + LOG_WARN("ADC calibration aborted, unreasonable voltage: %.2fV", measuredV); + break; + } + + // Determine the base multiplier currently in effect + float baseMult = 0.0f; + + if (config.power.adc_multiplier_override > 0.0f) { + baseMult = config.power.adc_multiplier_override; + } +#ifdef ADC_MULTIPLIER + else { + baseMult = ADC_MULTIPLIER; + } +#endif + + if (baseMult <= 0.0f) { + LOG_WARN("ADC calibration failed: no base multiplier"); + break; + } + + // Target voltage considered 100% by UI + constexpr float TARGET_VOLTAGE = 4.19f; + + // Calculate new multiplier + float newMult = baseMult * (TARGET_VOLTAGE / measuredV); + + config.power.adc_multiplier_override = newMult; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + + LOG_INFO("ADC calibrated: measured=%.3fV base=%.4f new=%.4f", measuredV, baseMult, newMult); + + break; + } + + // Display + case TOGGLE_DISPLAY_UNITS: + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; + else + config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; + + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; + + // Bluetooth + case TOGGLE_BLUETOOTH: + config.bluetooth.enabled = !config.bluetooth.enabled; + + if (config.bluetooth.enabled) { + // Switch behavior: Bluetooth ON forces WiFi OFF + config.network.wifi_enabled = false; + } + + nodeDB->saveToDisk(SEGMENT_CONFIG); + InkHUD::InkHUD::getInstance()->notifyApplyingChanges(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case TOGGLE_BLUETOOTH_PAIR_MODE: + config.bluetooth.fixed_pin = !config.bluetooth.fixed_pin; + nodeDB->saveToDisk(SEGMENT_CONFIG); + break; + + // Regions + case SET_REGION_US: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + break; + + case SET_REGION_EU_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + break; + + case SET_REGION_EU_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_433); + break; + + case SET_REGION_CN: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_CN); + break; + + case SET_REGION_JP: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_JP); + break; + + case SET_REGION_ANZ: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ); + break; + case SET_REGION_KR: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KR); + break; + + case SET_REGION_TW: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TW); + break; + + case SET_REGION_RU: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_RU); + break; + + case SET_REGION_IN: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_IN); + break; + + case SET_REGION_NZ_865: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NZ_865); + break; + + case SET_REGION_TH: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_TH); + break; + + case SET_REGION_LORA_24: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + break; + + case SET_REGION_UA_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_433); + break; + + case SET_REGION_UA_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_UA_868); + break; + + case SET_REGION_MY_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_433); + break; + + case SET_REGION_MY_919: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_MY_919); + break; + + case SET_REGION_SG_923: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_SG_923); + break; + + case SET_REGION_PH_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_433); + break; + + case SET_REGION_PH_868: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_868); + break; + + case SET_REGION_PH_915: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_PH_915); + break; + + case SET_REGION_ANZ_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ANZ_433); + break; + + case SET_REGION_KZ_433: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_433); + break; + + case SET_REGION_KZ_863: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_KZ_863); + break; + + case SET_REGION_NP_865: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_NP_865); + break; + + case SET_REGION_BR_902: + applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_BR_902); + break; + + // Roles + case SET_ROLE_CLIENT: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT); + break; + + case SET_ROLE_CLIENT_MUTE: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE); + break; + + case SET_ROLE_ROUTER: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_ROUTER); + break; + + case SET_ROLE_REPEATER: + applyDeviceRole(meshtastic_Config_DeviceConfig_Role_REPEATER); + break; + + // Presets + case SET_PRESET_LONG_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW); + break; + + case SET_PRESET_LONG_MODERATE: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE); + break; + + case SET_PRESET_LONG_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST); + break; + + case SET_PRESET_MEDIUM_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW); + break; + + case SET_PRESET_MEDIUM_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); + break; + + case SET_PRESET_SHORT_SLOW: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW); + break; + + case SET_PRESET_SHORT_FAST: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST); + break; + + case SET_PRESET_SHORT_TURBO: + applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + break; + + // Timezones + case SET_TZ_US_HAWAII: + applyTimezone("HST10"); + break; + + case SET_TZ_US_ALASKA: + applyTimezone("AKST9AKDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_PACIFIC: + applyTimezone("PST8PDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_ARIZONA: + applyTimezone("MST7"); + break; + + case SET_TZ_US_MOUNTAIN: + applyTimezone("MST7MDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_CENTRAL: + applyTimezone("CST6CDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_US_EASTERN: + applyTimezone("EST5EDT,M3.2.0,M11.1.0"); + break; + + case SET_TZ_BR_BRAZILIA: + applyTimezone("BRT3"); + break; + + case SET_TZ_UTC: + applyTimezone("UTC0"); + break; + + case SET_TZ_EU_WESTERN: + applyTimezone("GMT0BST,M3.5.0/1,M10.5.0"); + break; + + case SET_TZ_EU_CENTRAL: + applyTimezone("CET-1CEST,M3.5.0,M10.5.0/3"); + break; + + case SET_TZ_EU_EASTERN: + applyTimezone("EET-2EEST,M3.5.0/3,M10.5.0/4"); + break; + + case SET_TZ_ASIA_KOLKATA: + applyTimezone("IST-5:30"); + break; + + case SET_TZ_ASIA_HONG_KONG: + applyTimezone("HKT-8"); + break; + + case SET_TZ_AU_AWST: + applyTimezone("AWST-8"); + break; + + case SET_TZ_AU_ACST: + applyTimezone("ACST-9:30ACDT,M10.1.0,M4.1.0/3"); + break; + + case SET_TZ_AU_AEST: + applyTimezone("AEST-10AEDT,M10.1.0,M4.1.0/3"); + break; + + case SET_TZ_PACIFIC_NZ: + applyTimezone("NZST-12NZDT,M9.5.0,M4.1.0/3"); + break; + + // Channels + case TOGGLE_CHANNEL_UPLINK: { + auto &ch = channels.getByIndex(selectedChannelIndex); + ch.settings.uplink_enabled = !ch.settings.uplink_enabled; + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case TOGGLE_CHANNEL_DOWNLINK: { + auto &ch = channels.getByIndex(selectedChannelIndex); + ch.settings.downlink_enabled = !ch.settings.downlink_enabled; + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case TOGGLE_CHANNEL_POSITION: { + auto &ch = channels.getByIndex(selectedChannelIndex); + + if (!ch.settings.has_module_settings) + ch.settings.has_module_settings = true; + + if (ch.settings.module_settings.position_precision > 0) + ch.settings.module_settings.position_precision = 0; + else + ch.settings.module_settings.position_precision = 13; // default + + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case SET_CHANNEL_PRECISION: { + auto &ch = channels.getByIndex(selectedChannelIndex); + + if (!ch.settings.has_module_settings) + ch.settings.has_module_settings = true; + + // Cursor - 1 because of "Back" + uint8_t index = cursor - 1; + + constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); + + if (index < optionCount) { + ch.settings.module_settings.position_precision = POSITION_PRECISION_OPTIONS[index].value; + } + + nodeDB->saveToDisk(SEGMENT_CHANNELS); + service->reloadConfig(SEGMENT_CHANNELS); + break; + } + + case RESET_NODEDB_ALL: + InkHUD::getInstance()->notifyApplyingChanges(); + nodeDB->resetNodes(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + + case RESET_NODEDB_KEEP_FAVORITES: + InkHUD::getInstance()->notifyApplyingChanges(); + nodeDB->resetNodes(1); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + break; + default: LOG_WARN("Action not implemented"); } @@ -279,6 +864,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) { items.clear(); items.shrink_to_fit(); + nodeConfigLabels.clear(); switch (page) { case ROOT: @@ -289,6 +875,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back(MenuItem("Send", MenuPage::SEND)); items.push_back(MenuItem("Options", MenuPage::OPTIONS)); // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO + items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG)); items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::EXIT; @@ -305,6 +892,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case OPTIONS: + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT)); // Optional: backlight if (settings->optionalMenuItems.backlight) items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label @@ -312,16 +900,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) MenuPage::EXIT // Exit once complete )); - // Optional: GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED) - items.push_back(MenuItem("Enable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - items.push_back(MenuItem("Disable GPS", MenuAction::TOGGLE_GPS, MenuPage::EXIT)); - - // Optional: Enable Bluetooth, in case of lost wifi connection - if (!config.bluetooth.enabled || config.network.wifi_enabled) - items.push_back(MenuItem("Enable Bluetooth", MenuAction::ENABLE_BLUETOOTH, MenuPage::EXIT)); - + // Options Toggles items.push_back(MenuItem("Applets", MenuPage::APPLETS)); items.push_back(MenuItem("Auto-show", MenuPage::AUTOSHOW)); items.push_back(MenuItem("Recents Duration", MenuPage::RECENTS)); @@ -334,33 +913,423 @@ void InkHUD::MenuApplet::showPage(MenuPage page) &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, &settings->optionalFeatures.batteryIcon)); - invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); - - items.push_back( - MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::ROOT; break; case APPLETS: - populateAppletPage(); + populateAppletPage(); // must be first + items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::OPTIONS; break; case AUTOSHOW: - populateAutoshowPage(); + populateAutoshowPage(); // must be first + items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::OPTIONS; break; case RECENTS: - populateRecentsPage(); - previousPage = MenuPage::OPTIONS; + populateRecentsPage(); // builds only the options + items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; + case NODE_CONFIG: + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT)); + // Radio Config Section + items.push_back(MenuItem::Header("Radio Config")); + items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA)); + items.push_back(MenuItem("Channel", MenuPage::NODE_CONFIG_CHANNELS)); + // Device Config Section + items.push_back(MenuItem::Header("Device Config")); + items.push_back(MenuItem("Device", MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Position", MenuPage::NODE_CONFIG_POSITION)); + items.push_back(MenuItem("Power", MenuPage::NODE_CONFIG_POWER)); +#if defined(ARCH_ESP32) + items.push_back(MenuItem("Network", MenuPage::NODE_CONFIG_NETWORK)); +#endif + items.push_back(MenuItem("Display", MenuPage::NODE_CONFIG_DISPLAY)); + items.push_back(MenuItem("Bluetooth", MenuPage::NODE_CONFIG_BLUETOOTH)); + + // Administration Section + items.push_back(MenuItem::Header("Administration")); + items.push_back(MenuItem("Reset NodeDB", MenuPage::NODE_CONFIG_ADMIN_RESET)); + + // Exit + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_DEVICE: { + + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + const char *role = DisplayFormatters::getDeviceRole(config.device.role); + nodeConfigLabels.emplace_back("Role: " + std::string(role)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_DEVICE_ROLE)); + + const char *tzLabel = getTimezoneLabelFromValue(config.device.tzdef); + nodeConfigLabels.emplace_back("Timezone: " + std::string(tzLabel)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::TIMEZONE)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POSITION: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); +#if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS + const auto mode = config.position.gps_mode; + if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + items.push_back(MenuItem("GPS None", MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POSITION)); + } else { + gpsEnabled = (mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED); + items.push_back(MenuItem("GPS", MenuAction::TOGGLE_GPS, MenuPage::NODE_CONFIG_POSITION, &gpsEnabled)); + } +#endif + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POWER: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); +#if defined(ARCH_ESP32) + items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving)); +#endif + // ADC Multiplier + float effectiveMult = 0.0f; + + // User override always shows if it exists + if (config.power.adc_multiplier_override > 0.0f) { + effectiveMult = config.power.adc_multiplier_override; + } +#ifdef ADC_MULTIPLIER + else { + // Fallback to variant defined + effectiveMult = ADC_MULTIPLIER; + } +#endif + + // Only show if we actually have a value + if (effectiveMult > 0.0f) { + char buf[32]; + snprintf(buf, sizeof(buf), "ADC Mult: %.3f", effectiveMult); + nodeConfigLabels.emplace_back(buf); + + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_POWER_ADC_CAL)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_POWER_ADC_CAL: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER)); + + // Instruction text (header-style, non-selectable) + items.push_back(MenuItem::Header("Run on full charge Only")); + + // Action + items.push_back(MenuItem("Calibrate ADC", MenuAction::CALIBRATE_ADC, MenuPage::NODE_CONFIG_POWER)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_NETWORK: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off"; + + items.push_back(MenuItem(wifiLabel, MenuAction::TOGGLE_WIFI, MenuPage::EXIT)); + +#if HAS_WIFI && defined(ARCH_ESP32) + if (config.network.wifi_enabled) { + + // Status + if (WiFi.status() == WL_CONNECTED) { + nodeConfigLabels.emplace_back("Status: Connected"); + } else { + nodeConfigLabels.emplace_back("Status: Not Connected"); + } + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + + // Signal + if (WiFi.status() == WL_CONNECTED) { + int rssi = WiFi.RSSI(); + int quality = constrain(2 * (rssi + 100), 0, 100); + + char sigBuf[32]; + snprintf(sigBuf, sizeof(sigBuf), "Signal: %d%%", quality); + nodeConfigLabels.emplace_back(sigBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + + char ipBuf[64]; + snprintf(ipBuf, sizeof(ipBuf), "IP: %s", WiFi.localIP().toString().c_str()); + nodeConfigLabels.emplace_back(ipBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + + // SSID + if (config.network.wifi_ssid && strlen(config.network.wifi_ssid) > 0) { + std::string ssidLabel = "SSID: "; + ssidLabel += config.network.wifi_ssid; + nodeConfigLabels.emplace_back(ssidLabel); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + + // Hostname + const char *host = WiFi.getHostname(); + if (host && strlen(host) > 0) { + std::string hostLabel = "Host: "; + hostLabel += host; + nodeConfigLabels.emplace_back(hostLabel); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_NETWORK)); + } + } +#endif + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_DISPLAY: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY, + &config.display.use_12h_clock)); + + const char *unitsLabel = + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "Units: Imperial" : "Units: Metric"; + + items.push_back(MenuItem(unitsLabel, MenuAction::TOGGLE_DISPLAY_UNITS, MenuPage::NODE_CONFIG_DISPLAY)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_BLUETOOTH: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off"; + items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT)); + + const char *pairLabel = config.bluetooth.fixed_pin ? "Pair Mode: Fixed" : "Pair Mode: Random"; + items.push_back(MenuItem(pairLabel, MenuAction::TOGGLE_BLUETOOTH_PAIR_MODE, MenuPage::NODE_CONFIG_BLUETOOTH)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_LORA: { + + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + const char *region = myRegion ? myRegion->name : "Unset"; + nodeConfigLabels.emplace_back("Region: " + std::string(region)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::REGION)); + + const char *preset = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + nodeConfigLabels.emplace_back("Preset: " + std::string(preset)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_PRESET)); + + char freqBuf[32]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqBuf, sizeof(freqBuf), "Freq: %.3f MHz", freq); + nodeConfigLabels.emplace_back(freqBuf); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_LORA)); + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNELS: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + meshtastic_Channel &ch = channels.getByIndex(i); + + if (!ch.has_settings) + continue; + + if (ch.role == meshtastic_Channel_Role_DISABLED) + continue; + + std::string label = "#"; + + if (ch.role == meshtastic_Channel_Role_PRIMARY) { + label += "Primary"; + } else if (strlen(ch.settings.name) > 0) { + label += parse(ch.settings.name); + } else { + label += "Channel" + to_string(i + 1); + } + + nodeConfigLabels.push_back(label); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNEL_DETAIL: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS)); + + meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + + // Name (read-only) + const char *name = strlen(ch.settings.name) > 0 ? ch.settings.name : "Unnamed"; + nodeConfigLabels.emplace_back("Ch: " + parse(name)); + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + + // Uplink + items.push_back(MenuItem("Uplink", MenuAction::TOGGLE_CHANNEL_UPLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &ch.settings.uplink_enabled)); + + items.push_back(MenuItem("Downlink", MenuAction::TOGGLE_CHANNEL_DOWNLINK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &ch.settings.downlink_enabled)); + + // Position + channelPositionEnabled = ch.settings.has_module_settings && ch.settings.module_settings.position_precision > 0; + + items.push_back(MenuItem("Position", MenuAction::TOGGLE_CHANNEL_POSITION, MenuPage::NODE_CONFIG_CHANNEL_DETAIL, + &channelPositionEnabled)); + + // Precision + if (channelPositionEnabled) { + + std::string precisionLabel = "Unknown"; + + for (const auto &opt : POSITION_PRECISION_OPTIONS) { + if (opt.value == ch.settings.module_settings.position_precision) { + precisionLabel = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + ? opt.imperial + : opt.metric; + break; + } + } + nodeConfigLabels.emplace_back("Precision: " + precisionLabel); + items.push_back( + MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_CHANNEL_PRECISION)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_CHANNEL_PRECISION: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) { + items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + break; + } + constexpr uint8_t optionCount = sizeof(POSITION_PRECISION_OPTIONS) / sizeof(POSITION_PRECISION_OPTIONS[0]); + for (uint8_t i = 0; i < optionCount; i++) { + const auto &opt = POSITION_PRECISION_OPTIONS[i]; + const char *label = + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? opt.imperial : opt.metric; + nodeConfigLabels.emplace_back(label); + + items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_CHANNEL_PRECISION, + MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); + } + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case NODE_CONFIG_DEVICE_ROLE: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT)); + items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT)); + items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT)); + items.push_back(MenuItem("Repeater", MenuAction::SET_ROLE_REPEATER, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + + case TIMEZONE: + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Arizona", SET_TZ_US_ARIZONA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Mountain", SET_TZ_US_MOUNTAIN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Central", SET_TZ_US_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("US/Eastern", SET_TZ_US_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("BR/Brasilia", SET_TZ_BR_BRAZILIA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("UTC", SET_TZ_UTC, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Western", SET_TZ_EU_WESTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Central", SET_TZ_EU_CENTRAL, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("EU/Eastern", SET_TZ_EU_EASTERN, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Asia/Kolkata", SET_TZ_ASIA_KOLKATA, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Asia/Hong Kong", SET_TZ_ASIA_HONG_KONG, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/AWST", SET_TZ_AU_AWST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/ACST", SET_TZ_AU_ACST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("AU/AEST", SET_TZ_AU_AEST, MenuPage::NODE_CONFIG_DEVICE)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case REGION: + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA)); + items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT)); + items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT)); + items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT)); + items.push_back(MenuItem("CN", MenuAction::SET_REGION_CN, MenuPage::EXIT)); + items.push_back(MenuItem("JP", MenuAction::SET_REGION_JP, MenuPage::EXIT)); + items.push_back(MenuItem("ANZ", MenuAction::SET_REGION_ANZ, MenuPage::EXIT)); + items.push_back(MenuItem("KR", MenuAction::SET_REGION_KR, MenuPage::EXIT)); + items.push_back(MenuItem("TW", MenuAction::SET_REGION_TW, MenuPage::EXIT)); + items.push_back(MenuItem("RU", MenuAction::SET_REGION_RU, MenuPage::EXIT)); + items.push_back(MenuItem("IN", MenuAction::SET_REGION_IN, MenuPage::EXIT)); + items.push_back(MenuItem("NZ 865", MenuAction::SET_REGION_NZ_865, MenuPage::EXIT)); + items.push_back(MenuItem("TH", MenuAction::SET_REGION_TH, MenuPage::EXIT)); + items.push_back(MenuItem("LoRa 2.4", MenuAction::SET_REGION_LORA_24, MenuPage::EXIT)); + items.push_back(MenuItem("UA 433", MenuAction::SET_REGION_UA_433, MenuPage::EXIT)); + items.push_back(MenuItem("UA 868", MenuAction::SET_REGION_UA_868, MenuPage::EXIT)); + items.push_back(MenuItem("MY 433", MenuAction::SET_REGION_MY_433, MenuPage::EXIT)); + items.push_back(MenuItem("MY 919", MenuAction::SET_REGION_MY_919, MenuPage::EXIT)); + items.push_back(MenuItem("SG 923", MenuAction::SET_REGION_SG_923, MenuPage::EXIT)); + items.push_back(MenuItem("PH 433", MenuAction::SET_REGION_PH_433, MenuPage::EXIT)); + items.push_back(MenuItem("PH 868", MenuAction::SET_REGION_PH_868, MenuPage::EXIT)); + items.push_back(MenuItem("PH 915", MenuAction::SET_REGION_PH_915, MenuPage::EXIT)); + items.push_back(MenuItem("ANZ 433", MenuAction::SET_REGION_ANZ_433, MenuPage::EXIT)); + items.push_back(MenuItem("KZ 433", MenuAction::SET_REGION_KZ_433, MenuPage::EXIT)); + items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT)); + items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT)); + items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + case NODE_CONFIG_PRESET: { + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA)); + items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT)); + items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT)); + items.push_back(MenuItem("Medium Fast", MenuAction::SET_PRESET_MEDIUM_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Short Slow", MenuAction::SET_PRESET_SHORT_SLOW, MenuPage::EXIT)); + items.push_back(MenuItem("Short Fast", MenuAction::SET_PRESET_SHORT_FAST, MenuPage::EXIT)); + items.push_back(MenuItem("Short Turbo", MenuAction::SET_PRESET_SHORT_TURBO, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + } + // Administration Section + case NODE_CONFIG_ADMIN_RESET: + items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT)); + items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); + break; + + // Exit case EXIT: sendToBackground(); // Menu applet dismissed, allow normal behavior to resume break; @@ -379,6 +1348,15 @@ void InkHUD::MenuApplet::showPage(MenuPage page) cursorShown = false; } + // Ensure cursor never rests on a header + if (cursorShown) { + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) + cursor = 0; + } + // Remember which page we are on now currentPage = page; } @@ -390,7 +1368,8 @@ void InkHUD::MenuApplet::onRender() // Dimensions for the slots where we will draw menuItems const float padding = 0.05; - const uint16_t itemH = fontSmall.lineHeight() * 2; + const uint16_t itemH = fontSmall.lineHeight() * 1.6; + const int16_t selectInsetY = 2; const int16_t itemW = width() - X(padding) - X(padding); const int16_t itemL = X(padding); const int16_t itemR = X(1 - padding); @@ -441,18 +1420,31 @@ void InkHUD::MenuApplet::onRender() // -- Loop: draw each (visible) menu item -- for (uint8_t i = firstItem; i <= lastItem; i++) { - // Grab the menuItem - MenuItem item = items.at(i); - // Center-line for the text + // Grab the menu item + MenuItem &item = items.at(i); + + // Vertical center of this slot int16_t center = itemT + (itemH / 2); - // Box, if currently selected - if (cursorShown && i == cursor) - drawRect(itemL, itemT, itemW, itemH, BLACK); + // Header (non-selectable section label) + if (item.isHeader) { + setFont(fontSmall); - // Item's text - printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + // Header text (flush left) + printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE); + + // Subtle underline + int16_t underlineY = itemT + itemH - 2; + drawLine(itemL + X(padding), underlineY, itemR - X(padding), underlineY, BLACK); + } else { + // Box, if currently selected + if (cursorShown && i == cursor) + drawRect(itemL, itemT + selectInsetY, itemW, itemH - (selectInsetY * 2), BLACK); + + // Indented normal item text + printAt(itemL + X(padding * 2), center, item.label, LEFT, MIDDLE); + } // Checkbox, if relevant if (item.checkState) { @@ -493,11 +1485,14 @@ void InkHUD::MenuApplet::onButtonShortPress() OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (!settings->joystick.enabled) { - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else + if (!cursorShown) { cursorShown = true; + cursor = 0; + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } else { if (cursorShown) @@ -538,14 +1533,17 @@ void InkHUD::MenuApplet::onNavUp() { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Move menu cursor to previous entry, then update - if (cursor == 0) - cursor = items.size() - 1; - else - cursor--; - - if (!cursorShown) + if (!cursorShown) { cursorShown = true; + cursor = 0; + } else { + do { + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + } while (items.at(cursor).isHeader); + } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -554,11 +1552,14 @@ void InkHUD::MenuApplet::onNavDown() { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else + if (!cursorShown) { cursorShown = true; + cursor = 0; + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -622,7 +1623,8 @@ void InkHUD::MenuApplet::populateRecentsPage() // (Defined at top of this file) for (uint8_t i = 0; i < optionCount; i++) { std::string label = to_string(RECENTS_OPTIONS_MINUTES[i]) + " mins"; - items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::EXIT)); + recentsSelected[i] = (settings->recentlyActiveSeconds == RECENTS_OPTIONS_MINUTES[i] * 60); + items.push_back(MenuItem(label.c_str(), MenuAction::SET_RECENTS, MenuPage::OPTIONS, &recentsSelected[i])); } } @@ -873,5 +1875,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources() cm.messageItems.clear(); cm.recipientItems.clear(); } - -#endif +#endif // MESHTASTIC_INCLUDE_INKHUD \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 4f9f92227..82ccc8f45 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -35,6 +35,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onRender() override; void show(Tile *t); // Open the menu, onto a user tile + void setStartPage(MenuPage page); protected: Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton @@ -56,6 +57,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data + MenuPage startPageOverride = MenuPage::ROOT; MenuPage currentPage = MenuPage::ROOT; MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted @@ -63,7 +65,15 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread uint16_t systemInfoPanelHeight = 0; // Need to know before we render - std::vector items; // MenuItems for the current page. Filled by ShowPage + std::vector items; // MenuItems for the current page. Filled by ShowPage + std::vector nodeConfigLabels; // Persistent labels for Node Config pages + uint8_t selectedChannelIndex = 0; // Currently selected LoRa channel (Node Config → Radio → Channel) + bool channelPositionEnabled = false; + bool gpsEnabled = false; + + // Recents menu checkbox state (derived from settings.recentlyActiveSeconds) + static constexpr uint8_t RECENTS_COUNT = 6; + bool recentsSelected[RECENTS_COUNT] = {}; // Data for selecting and sending canned messages via the menu // Placed into a sub-class for organization only diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h index c74fe3d8a..51c9161a7 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuItem.h @@ -30,6 +30,7 @@ class MenuItem MenuAction action = NO_ACTION; MenuPage nextPage = EXIT; bool *checkState = nullptr; + bool isHeader = false; // Non-selectable section label // Various constructors, depending on the intended function of the item @@ -40,6 +41,12 @@ class MenuItem : label(label), action(action), nextPage(nextPage), checkState(checkState) { } + static MenuItem Header(const char *label) + { + MenuItem item(label, NO_ACTION, EXIT); + item.isHeader = true; + return item; + } }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index 389e411c3..138c389be 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -20,10 +20,27 @@ enum MenuPage : uint8_t { SEND, CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, + NODE_CONFIG, + NODE_CONFIG_LORA, + NODE_CONFIG_CHANNELS, // List of channels + NODE_CONFIG_CHANNEL_DETAIL, // Per-channel options + NODE_CONFIG_CHANNEL_PRECISION, + NODE_CONFIG_PRESET, + NODE_CONFIG_DEVICE, + NODE_CONFIG_DEVICE_ROLE, + NODE_CONFIG_POWER, + NODE_CONFIG_POWER_ADC_CAL, + NODE_CONFIG_NETWORK, + NODE_CONFIG_DISPLAY, + NODE_CONFIG_BLUETOOTH, + NODE_CONFIG_POSITION, + NODE_CONFIG_ADMIN_RESET, + TIMEZONE, APPLETS, AUTOSHOW, RECENTS, // Select length of "recentlyActiveSeconds" - EXIT, // Dismiss the menu applet + REGION, + EXIT, // Dismiss the menu applet }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index a9d579873..7869319fe 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -10,34 +10,37 @@ using namespace NicheGraphics; InkHUD::TipsApplet::TipsApplet() { - // Decide which tips (if any) should be shown to user after the boot screen + bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + + bool showTutorialTips = (settings->tips.firstBoot || needsRegion); // Welcome screen - if (settings->tips.firstBoot) + if (showTutorialTips) tipQueue.push_back(Tip::WELCOME); - // Antenna, region, timezone - // Shown at boot if region not yet set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + // Finish setup + if (needsRegion) tipQueue.push_back(Tip::FINISH_SETUP); + // Using the UI + if (showTutorialTips) { + tipQueue.push_back(Tip::CUSTOMIZATION); + tipQueue.push_back(Tip::BUTTONS); + } + // Shutdown info // Shown until user performs one valid shutdown if (!settings->tips.safeShutdownSeen) tipQueue.push_back(Tip::SAFE_SHUTDOWN); - // Using the UI - if (settings->tips.firstBoot) { - tipQueue.push_back(Tip::CUSTOMIZATION); - tipQueue.push_back(Tip::BUTTONS); - } - // Catch an incorrect attempt at rotating display if (config.display.flip_screen) tipQueue.push_back(Tip::ROTATION); - // Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground - // LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector + // Region picker + if (needsRegion) + tipQueue.push_back(Tip::PICK_REGION); + if (!tipQueue.empty()) bringToForeground(); } @@ -51,81 +54,109 @@ void InkHUD::TipsApplet::onRender() case Tip::FINISH_SETUP: { setFont(fontMedium); - printAt(0, 0, "Tip: Finish Setup"); + const char *title = "Tip: Finish Setup"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "- connect antenna"); + int16_t cursorY = h + fontSmall.lineHeight(); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- connect a client app"); + auto drawBullet = [&](const char *text) { + uint16_t bh = getWrappedTextHeight(0, width(), text); + printWrapped(0, cursorY, width(), text); + cursorY += bh + (fontSmall.lineHeight() / 3); + }; - // Only if region not set - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set region"); - } + drawBullet("- connect antenna"); + drawBullet("- connect a client app"); - // Only if tz not set - if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) { - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- set timezone"); - } + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + drawBullet("- set region"); - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "More info at meshtastic.org"); + if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) + drawBullet("- set timezone"); + + cursorY += fontSmall.lineHeight() / 2; + drawBullet("More info at meshtastic.org"); - setFont(fontSmall); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; + case Tip::PICK_REGION: { + setFont(fontMedium); + printAt(0, 0, "Set Region"); + + setFont(fontSmall); + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "Please select your LoRa region to complete setup."); + + printAt(0, Y(1.0), "Press button to choose", LEFT, BOTTOM); + } break; + case Tip::SAFE_SHUTDOWN: { setFont(fontMedium); - printAt(0, 0, "Tip: Shutdown"); + + const char *title = "Tip: Shutdown"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - std::string shutdown; - shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n"; - shutdown += "\n"; - shutdown += "This ensures data is saved."; - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown); + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "Before removing power, please shut down from InkHUD menu, or a client app.\n\n" + "This ensures data is saved."; + + uint16_t bodyH = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); - } break; case Tip::CUSTOMIZATION: { setFont(fontMedium); - printAt(0, 0, "Tip: Customization"); + + const char *title = "Tip: Customization"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more."); + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "Configure & control display with the InkHUD menu. " + "Optional features, layout, rotation, and more."; + + uint16_t bodyH = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bodyH + (fontSmall.lineHeight() / 2); printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; case Tip::BUTTONS: { setFont(fontMedium); - printAt(0, 0, "Tip: Buttons"); + + const char *title = "Tip: Buttons"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); - int16_t cursorY = fontMedium.lineHeight() * 1.5; + int16_t cursorY = h + fontSmall.lineHeight(); + + auto drawBullet = [&](const char *text) { + uint16_t bh = getWrappedTextHeight(0, width(), text); + printWrapped(0, cursorY, width(), text); + cursorY += bh + (fontSmall.lineHeight() / 3); + }; if (!settings->joystick.enabled) { - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); + drawBullet("User Button"); + drawBullet("- short press: next"); + drawBullet("- long press: select or open menu"); } else { - printAt(0, cursorY, "Joystick"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- open menu / select"); - cursorY += fontSmall.lineHeight() * 1.5; - printAt(0, cursorY, "Exit Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- switch tile / close menu"); + drawBullet("Joystick"); + drawBullet("- press: open menu or select"); + drawBullet("Exit Button"); + drawBullet("- press: switch tile or close menu"); } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); @@ -133,12 +164,21 @@ void InkHUD::TipsApplet::onRender() case Tip::ROTATION: { setFont(fontMedium); - printAt(0, 0, "Tip: Rotation"); + + const char *title = "Tip: Rotation"; + uint16_t h = getWrappedTextHeight(0, width(), title); + printWrapped(0, 0, width(), title); setFont(fontSmall); if (!settings->joystick.enabled) { - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + int16_t cursorY = h + fontSmall.lineHeight(); + + const char *body = "To rotate the display, use the InkHUD menu. " + "Long-press the user button > Options > Rotate."; + + uint16_t bh = getWrappedTextHeight(0, width(), body); + printWrapped(0, cursorY, width(), body); + cursorY += bh + (fontSmall.lineHeight() / 2); } else { printWrapped(0, fontMedium.lineHeight() * 1.5, width(), "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); @@ -159,12 +199,15 @@ void InkHUD::TipsApplet::renderWelcome() { uint16_t padW = X(0.05); + // Detect portrait orientation + bool portrait = height() > width(); + // Block 1 - logo & title // ======================== // Logo size - uint16_t logoWLimit = X(0.3); - uint16_t logoHLimit = Y(0.3); + uint16_t logoWLimit = portrait ? X(0.5) : X(0.3); + uint16_t logoHLimit = portrait ? Y(0.25) : Y(0.3); uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit); uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit); @@ -177,7 +220,7 @@ void InkHUD::TipsApplet::renderWelcome() // Center the block // Desired effect: equal margin from display edge for logo left and title right - int16_t block1Y = Y(0.3); + int16_t block1Y = portrait ? Y(0.2) : Y(0.3); int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2); int16_t logoCX = block1CX - (logoW / 2) - (padW / 2); int16_t titleCX = block1CX + (titleW / 2) + (padW / 2); @@ -192,7 +235,7 @@ void InkHUD::TipsApplet::renderWelcome() std::string subtitle = "InkHUD"; if (width() >= 200) subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display - printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE); + printAt(X(0.5), portrait ? Y(0.45) : Y(0.6), subtitle, CENTER, MIDDLE); // Block 3 - press to continue // ============================ @@ -224,26 +267,37 @@ void InkHUD::TipsApplet::onBackground() // While our SystemApplet::handleInput flag is true void InkHUD::TipsApplet::onButtonShortPress() { + bool needsRegion = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET); + // If we're prompting the user to pick a region, hand off to the menu + if (!tipQueue.empty() && tipQueue.front() == Tip::PICK_REGION) { + tipQueue.pop_front(); + + // Signal InkHUD to open the menu on Region page + inkhud->forceRegionMenu = true; + + // Close tips and open menu + sendToBackground(); + inkhud->openMenu(); + return; + } + // Consume current tip tipQueue.pop_front(); // All tips done if (tipQueue.empty()) { // Record that user has now seen the "tutorial" set of tips // Don't show them on subsequent boots - if (settings->tips.firstBoot) { + if (settings->tips.firstBoot && !needsRegion) { settings->tips.firstBoot = false; inkhud->persistence->saveSettings(); } - // Close applet, and full refresh to clean the screen - // Need to force update, because our request would be ignored otherwise, as we are now background + // Close applet and clean the screen sendToBackground(); inkhud->forceUpdate(EInk::UpdateTypes::FULL); - } - - // More tips left - else + } else { requestUpdate(); + } } // Functions the same as the user button in this instance diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index 159e6f58f..ff7eea046 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -23,6 +23,7 @@ class TipsApplet : public SystemApplet enum class Tip { WELCOME, FINISH_SETUP, + PICK_REGION, SAFE_SHUTDOWN, CUSTOMIZATION, BUTTONS, diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index 5382d2391..fa45a49ed 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -276,6 +276,15 @@ int InkHUD::Events::beforeDeepSleep(void *unused) return 0; // We agree: deep sleep now } +// Display an intermediate screen while configuration changes are applied +void InkHUD::Events::applyingChanges() +{ + // Bring the logo applet forward with a temporary message + for (SystemApplet *sa : inkhud->systemApplets) { + sa->onApplyingChanges(); + } +} + // Callback for rebootObserver // Same as shutdown, without drawing the logoApplet // Makes sure we don't lose message history / InkHUD config diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 664ca19f0..1916cf78e 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -29,12 +29,13 @@ class Events void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press - void onExitShort(); // Exit button: short press - void onExitLong(); // Exit button: long press - void onNavUp(); // Navigate up - void onNavDown(); // Navigate down - void onNavLeft(); // Navigate left - void onNavRight(); // Navigate right + void applyingChanges(); + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 9f05ae5bb..13b15b7e8 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -53,6 +53,13 @@ void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile); } +void InkHUD::InkHUD::notifyApplyingChanges() +{ + if (events) { + events->applyingChanges(); + } +} + // Start InkHUD! // Call this only after you have configured InkHUD void InkHUD::InkHUD::begin() diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 7325d8262..5280d9ac7 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -47,6 +47,7 @@ class InkHUD void setDriver(Drivers::EInk *driver); void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0); void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1); + void notifyApplyingChanges(); void begin(); @@ -76,6 +77,9 @@ class InkHUD void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); + // Used by TipsApplet to force menu to start on Region selection + bool forceRegionMenu = false; + // Updating the display // - called by various InkHUD components diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h index 7ee47eeb9..fb5b06e51 100644 --- a/src/graphics/niche/InkHUD/SystemApplet.h +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -27,6 +27,7 @@ class SystemApplet : public Applet bool lockRequests = false; // - prevent other applets from triggering display updates virtual void onReboot() { onShutdown(); } // - handle reboot specially + virtual void onApplyingChanges() {} // Other system applets may take precedence over our own system applet though // The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank) From 91ad861b26edcd0382190b64ac49371d87860843 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 27 Jan 2026 09:56:56 -0600 Subject: [PATCH 17/31] Add Thinknode M4 variant_shutdown() (#9449) --- .../nrf52840/ELECROW-ThinkNode-M4/variant.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp index af9bed998..5c4b6215b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -49,3 +49,21 @@ void initVariant() pinMode(Battery_LED_4, OUTPUT); ledOff(Battery_LED_4); } + +/// called from main-nrf52.cpp during the cpuDeepSleep() function +void variant_shutdown() +{ + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == PIN_BUTTON1) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } + digitalWrite(PIN_GPS_EN, HIGH); +} From b6a1020fc522fc07ec7c39b5325206f21bf84dc8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 27 Jan 2026 13:06:50 -0600 Subject: [PATCH 18/31] Add error handling for SPI command failures in LR11x0, RF95, and SX128x interfaces (#9447) --- src/mesh/LR11x0Interface.cpp | 13 ++++++++++++- src/mesh/RF95Interface.cpp | 3 +++ src/mesh/SX128xInterface.cpp | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 341afe78d..a8a2780ed 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -91,10 +91,21 @@ template bool LR11x0Interface::init() LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); #endif + // Allow extra time for TCXO to stabilize after power-on + delay(10); + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + + // Retry if we get SPI command failed - some units need extra TCXO stabilization time + if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { + LOG_WARN("LR11x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res); + delay(100); + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } + // \todo Display actual typename of the adapter, not just `LR11x0` LOG_INFO("LR11x0 init result %d", res); - if (res == RADIOLIB_ERR_CHIP_NOT_FOUND) + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; LR11x0VersionInfo_t version; diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 5588fc348..0c12401ca 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -177,6 +177,9 @@ bool RF95Interface::init() int res = lora->begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); LOG_INFO("RF95 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; + LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index b4278c636..3ab63df3d 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -69,6 +69,8 @@ template bool SX128xInterface::init() int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); // \todo Display actual typename of the adapter, not just `SX128x` LOG_INFO("SX128x init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; if ((config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (res == RADIOLIB_ERR_INVALID_FREQUENCY)) { LOG_WARN("Radio only supports 2.4GHz LoRa. Adjusting Region and rebooting"); From d1edd386b65e297c1a96d78e9f06ecc3807db323 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:08:10 -0600 Subject: [PATCH 19/31] Update meshtastic/device-ui digest to 69739b8 (#9448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ee51d24ec..b7c46a6e1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -119,7 +119,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/37ad715b76cd6ca4aa500a4a4d9740e3cdf3e3cb.zip + https://github.com/meshtastic/device-ui/archive/69739b84f87a91568d3c421498bc89977937a141.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From cfda9bb8ef7026d8936d97799e5c53eeb6525613 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:12:03 -0600 Subject: [PATCH 20/31] Update protobufs (#9453) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/admin.pb.cpp | 9 ++ src/mesh/generated/meshtastic/admin.pb.h | 111 +++++++++++++++++- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/localonly.pb.h | 16 ++- .../generated/meshtastic/telemetry.pb.cpp | 3 + src/mesh/generated/meshtastic/telemetry.pb.h | 41 +++++++ 7 files changed, 173 insertions(+), 11 deletions(-) diff --git a/protobufs b/protobufs index d9003b2b6..bc63a57f9 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d9003b2b6c9f378066979ec83b013aca441cf240 +Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index e358bc96d..01d3fa910 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -27,6 +27,15 @@ PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO) PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) +PB_BIND(meshtastic_SensorConfig, meshtastic_SensorConfig, AUTO) + + +PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO) + + +PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index 26b4343e9..f545ed9bf 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -77,7 +77,9 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType { /* TODO: REPLACE */ meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG = 11, /* TODO: REPLACE */ - meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12 + meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12, + /* TODO: REPLACE */ + meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13 } meshtastic_AdminMessage_ModuleConfigType; typedef enum _meshtastic_AdminMessage_BackupLocation { @@ -169,6 +171,48 @@ typedef struct _meshtastic_KeyVerificationAdmin { uint32_t security_number; } meshtastic_KeyVerificationAdmin; +typedef struct _meshtastic_SCD4X_config { + /* Set Automatic self-calibration enabled */ + bool has_set_asc; + bool set_asc; + /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ + bool has_set_target_co2_conc; + uint32_t set_target_co2_conc; + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ + bool has_set_altitude; + uint32_t set_altitude; + /* Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude) */ + bool has_set_ambient_pressure; + uint32_t set_ambient_pressure; + /* Perform a factory reset of the sensor */ + bool has_factory_reset; + bool factory_reset; + /* Power mode for sensor (true for low power, false for normal) */ + bool has_set_power_mode; + bool set_power_mode; +} meshtastic_SCD4X_config; + +typedef struct _meshtastic_SEN5X_config { + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* One-shot mode (true for low power - one-shot mode, false for normal - continuous mode) */ + bool has_set_one_shot_mode; + bool set_one_shot_mode; +} meshtastic_SEN5X_config; + +typedef struct _meshtastic_SensorConfig { + /* SCD4X CO2 Sensor configuration */ + bool has_scd4x_config; + meshtastic_SCD4X_config scd4x_config; + /* SEN5X PM Sensor configuration */ + bool has_sen5x_config; + meshtastic_SEN5X_config sen5x_config; +} meshtastic_SensorConfig; + typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; /* This message is handled by the Admin module and is responsible for all settings/channel read/write operations. This message is used to do settings operations to both remote AND local nodes. @@ -301,6 +345,8 @@ typedef struct _meshtastic_AdminMessage { bool nodedb_reset; /* Tell the node to reset into the OTA Loader */ meshtastic_AdminMessage_OTAEvent ota_request; + /* Parameters and sensor configuration */ + meshtastic_SensorConfig sensor_config; }; /* The node generates this key and sends it with any get_x_response packets. The client MUST include the same key with any set_x commands. Key expires after 300 seconds. @@ -323,8 +369,8 @@ extern "C" { #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) #define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG+1)) +#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG+1)) #define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD @@ -349,6 +395,9 @@ extern "C" { #define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType + + + /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} @@ -357,6 +406,9 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} +#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default} +#define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SEN5X_config_init_default {false, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}} @@ -364,6 +416,9 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} +#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero} +#define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SEN5X_config_init_zero {false, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_AdminMessage_InputEvent_event_code_tag 1 @@ -385,6 +440,17 @@ extern "C" { #define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2 #define meshtastic_KeyVerificationAdmin_nonce_tag 3 #define meshtastic_KeyVerificationAdmin_security_number_tag 4 +#define meshtastic_SCD4X_config_set_asc_tag 1 +#define meshtastic_SCD4X_config_set_target_co2_conc_tag 2 +#define meshtastic_SCD4X_config_set_temperature_tag 3 +#define meshtastic_SCD4X_config_set_altitude_tag 4 +#define meshtastic_SCD4X_config_set_ambient_pressure_tag 5 +#define meshtastic_SCD4X_config_factory_reset_tag 6 +#define meshtastic_SCD4X_config_set_power_mode_tag 7 +#define meshtastic_SEN5X_config_set_temperature_tag 1 +#define meshtastic_SEN5X_config_set_one_shot_mode_tag 2 +#define meshtastic_SensorConfig_scd4x_config_tag 1 +#define meshtastic_SensorConfig_sen5x_config_tag 2 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -441,6 +507,7 @@ extern "C" { #define meshtastic_AdminMessage_factory_reset_config_tag 99 #define meshtastic_AdminMessage_nodedb_reset_tag 100 #define meshtastic_AdminMessage_ota_request_tag 102 +#define meshtastic_AdminMessage_sensor_config_tag 103 #define meshtastic_AdminMessage_session_passkey_tag 101 /* Struct field encoding specification for nanopb */ @@ -501,7 +568,8 @@ X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_se X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 102) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,sensor_config,sensor_config), 103) #define meshtastic_AdminMessage_CALLBACK NULL #define meshtastic_AdminMessage_DEFAULT NULL #define meshtastic_AdminMessage_payload_variant_get_channel_response_MSGTYPE meshtastic_Channel @@ -523,6 +591,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ota_request,ota_request), 10 #define meshtastic_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact #define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin #define meshtastic_AdminMessage_payload_variant_ota_request_MSGTYPE meshtastic_AdminMessage_OTAEvent +#define meshtastic_AdminMessage_payload_variant_sensor_config_MSGTYPE meshtastic_SensorConfig #define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ @@ -569,6 +638,31 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_KeyVerificationAdmin_CALLBACK NULL #define meshtastic_KeyVerificationAdmin_DEFAULT NULL +#define meshtastic_SensorConfig_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) +#define meshtastic_SensorConfig_CALLBACK NULL +#define meshtastic_SensorConfig_DEFAULT NULL +#define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config +#define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config + +#define meshtastic_SCD4X_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ +X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ +X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ +X(a, STATIC, OPTIONAL, UINT32, set_ambient_pressure, 5) \ +X(a, STATIC, OPTIONAL, BOOL, factory_reset, 6) \ +X(a, STATIC, OPTIONAL, BOOL, set_power_mode, 7) +#define meshtastic_SCD4X_config_CALLBACK NULL +#define meshtastic_SCD4X_config_DEFAULT NULL + +#define meshtastic_SEN5X_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 1) \ +X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) +#define meshtastic_SEN5X_config_CALLBACK NULL +#define meshtastic_SEN5X_config_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; @@ -576,6 +670,9 @@ extern const pb_msgdesc_t meshtastic_HamParameters_msg; extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; extern const pb_msgdesc_t meshtastic_SharedContact_msg; extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; +extern const pb_msgdesc_t meshtastic_SensorConfig_msg; +extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; +extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg @@ -585,6 +682,9 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; #define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg #define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg #define meshtastic_KeyVerificationAdmin_fields &meshtastic_KeyVerificationAdmin_msg +#define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg +#define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg +#define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size @@ -594,6 +694,9 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 +#define meshtastic_SCD4X_config_size 29 +#define meshtastic_SEN5X_config_size 7 +#define meshtastic_SensorConfig_size 40 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 409805d24..57e7df8fc 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2279 +#define meshtastic_BackupPreferences_size 2362 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 2b44d0c9a..f11b13419 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -87,6 +87,9 @@ typedef struct _meshtastic_LocalModuleConfig { /* Paxcounter Config */ bool has_paxcounter; meshtastic_ModuleConfig_PaxcounterConfig paxcounter; + /* StatusMessage Config */ + bool has_statusmessage; + meshtastic_ModuleConfig_StatusMessageConfig statusmessage; } meshtastic_LocalModuleConfig; @@ -96,9 +99,9 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default} -#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default} +#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default} #define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero} -#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero} +#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_LocalConfig_device_tag 1 @@ -124,6 +127,7 @@ extern "C" { #define meshtastic_LocalModuleConfig_ambient_lighting_tag 12 #define meshtastic_LocalModuleConfig_detection_sensor_tag 13 #define meshtastic_LocalModuleConfig_paxcounter_tag 14 +#define meshtastic_LocalModuleConfig_statusmessage_tag 15 /* Struct field encoding specification for nanopb */ #define meshtastic_LocalConfig_FIELDLIST(X, a) \ @@ -161,7 +165,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, remote_hardware, 10) \ X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \ -X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) +X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \ +X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) #define meshtastic_LocalModuleConfig_CALLBACK NULL #define meshtastic_LocalModuleConfig_DEFAULT NULL #define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -177,6 +182,7 @@ X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) #define meshtastic_LocalModuleConfig_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig #define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig +#define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig extern const pb_msgdesc_t meshtastic_LocalConfig_msg; extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; @@ -186,9 +192,9 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; #define meshtastic_LocalModuleConfig_fields &meshtastic_LocalModuleConfig_msg /* Maximum encoded size of messages (where known) */ -#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size +#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size #define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 675 +#define meshtastic_LocalModuleConfig_size 758 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.cpp b/src/mesh/generated/meshtastic/telemetry.pb.cpp index 345d7a157..fff75ebc1 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.cpp +++ b/src/mesh/generated/meshtastic/telemetry.pb.cpp @@ -33,6 +33,9 @@ PB_BIND(meshtastic_Telemetry, meshtastic_Telemetry, 2) PB_BIND(meshtastic_Nau7802Config, meshtastic_Nau7802Config, AUTO) +PB_BIND(meshtastic_SEN5XState, meshtastic_SEN5XState, AUTO) + + diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 131dd9949..dc9d876dc 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -435,6 +435,25 @@ typedef struct _meshtastic_Nau7802Config { float calibrationFactor; } meshtastic_Nau7802Config; +/* SEN5X State, for saving to flash */ +typedef struct _meshtastic_SEN5XState { + /* Last cleaning time for SEN5X */ + uint32_t last_cleaning_time; + /* Last cleaning time for SEN5X - valid flag */ + bool last_cleaning_valid; + /* Config flag for one-shot mode (see admin.proto) */ + bool one_shot_mode; + /* Last VOC state time for SEN55 */ + bool has_voc_state_time; + uint32_t voc_state_time; + /* Last VOC state validity flag for SEN55 */ + bool has_voc_state_valid; + bool voc_state_valid; + /* VOC state array (8x uint8t) for SEN55 */ + bool has_voc_state_array; + uint64_t voc_state_array; +} meshtastic_SEN5XState; + #ifdef __cplusplus extern "C" { @@ -455,6 +474,7 @@ extern "C" { + /* Initializer values for message structs */ #define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} @@ -465,6 +485,7 @@ extern "C" { #define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} #define meshtastic_Nau7802Config_init_default {0, 0} +#define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 0} #define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} @@ -474,6 +495,7 @@ extern "C" { #define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} #define meshtastic_Nau7802Config_init_zero {0, 0} +#define meshtastic_SEN5XState_init_zero {0, 0, 0, false, 0, false, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_DeviceMetrics_battery_level_tag 1 @@ -581,6 +603,12 @@ extern "C" { #define meshtastic_Telemetry_host_metrics_tag 8 #define meshtastic_Nau7802Config_zeroOffset_tag 1 #define meshtastic_Nau7802Config_calibrationFactor_tag 2 +#define meshtastic_SEN5XState_last_cleaning_time_tag 1 +#define meshtastic_SEN5XState_last_cleaning_valid_tag 2 +#define meshtastic_SEN5XState_one_shot_mode_tag 3 +#define meshtastic_SEN5XState_voc_state_time_tag 4 +#define meshtastic_SEN5XState_voc_state_valid_tag 5 +#define meshtastic_SEN5XState_voc_state_array_tag 6 /* Struct field encoding specification for nanopb */ #define meshtastic_DeviceMetrics_FIELDLIST(X, a) \ @@ -731,6 +759,16 @@ X(a, STATIC, SINGULAR, FLOAT, calibrationFactor, 2) #define meshtastic_Nau7802Config_CALLBACK NULL #define meshtastic_Nau7802Config_DEFAULT NULL +#define meshtastic_SEN5XState_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, last_cleaning_time, 1) \ +X(a, STATIC, SINGULAR, BOOL, last_cleaning_valid, 2) \ +X(a, STATIC, SINGULAR, BOOL, one_shot_mode, 3) \ +X(a, STATIC, OPTIONAL, UINT32, voc_state_time, 4) \ +X(a, STATIC, OPTIONAL, BOOL, voc_state_valid, 5) \ +X(a, STATIC, OPTIONAL, FIXED64, voc_state_array, 6) +#define meshtastic_SEN5XState_CALLBACK NULL +#define meshtastic_SEN5XState_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_DeviceMetrics_msg; extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg; extern const pb_msgdesc_t meshtastic_PowerMetrics_msg; @@ -740,6 +778,7 @@ extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; extern const pb_msgdesc_t meshtastic_HostMetrics_msg; extern const pb_msgdesc_t meshtastic_Telemetry_msg; extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; +extern const pb_msgdesc_t meshtastic_SEN5XState_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_DeviceMetrics_fields &meshtastic_DeviceMetrics_msg @@ -751,6 +790,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; #define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg #define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg #define meshtastic_Nau7802Config_fields &meshtastic_Nau7802Config_msg +#define meshtastic_SEN5XState_fields &meshtastic_SEN5XState_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size @@ -762,6 +802,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; #define meshtastic_LocalStats_size 87 #define meshtastic_Nau7802Config_size 16 #define meshtastic_PowerMetrics_size 81 +#define meshtastic_SEN5XState_size 27 #define meshtastic_Telemetry_size 272 #ifdef __cplusplus From 10b2eae70c7f36895892bbbd3094f2cb4544026d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 27 Jan 2026 13:56:32 -0600 Subject: [PATCH 21/31] Move more code out of main-nrf52 into variant.cpp (#9450) --- src/platform/nrf52/main-nrf52.cpp | 16 ---------------- .../nrf52840/ELECROW-ThinkNode-M1/variant.cpp | 7 +++++++ .../diy/nrf52_promicro_diy_tcxo/variant.cpp | 7 +++++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 11b05165c..0376a1dad 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -464,22 +464,6 @@ void cpuDeepSleep(uint32_t msecToWake) // FIXME, use non-init RAM per // https://devzone.nordicsemi.com/f/nordic-q-a/48919/ram-retention-settings-with-softdevice-enabled -#ifdef ELECROW_ThinkNode_M1 - nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); - - nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); - nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; - nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); -#endif - -#ifdef PROMICRO_DIY_TCXO - nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin - nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge - nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep -#endif - #ifdef BATTERY_LPCOMP_INPUT // Wake up if power rises again nrf_lpcomp_config_t c; diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index 2ce84bc57..1560cde73 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -59,4 +59,11 @@ void variant_shutdown() NRF_GPIO->DIRCLR = (1 << pin); } } + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense); + + nrf_gpio_cfg_input(PIN_BUTTON2, NRF_GPIO_PIN_PULLUP); + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON2, sense1); } \ No newline at end of file diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp index 5869ed1d4..384c618e2 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.cpp @@ -36,3 +36,10 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +void variant_shutdown() +{ + nrf_gpio_cfg_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP); // Enable internal pull-up on the button pin + nrf_gpio_pin_sense_t sense = NRF_GPIO_PIN_SENSE_LOW; // Configure SENSE signal on low edge + nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep +} From 23a8b5a66f9ad9f5176bd768beef2200c2f3066c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=9E=97?= <47050377+linser233@users.noreply.github.com> Date: Wed, 28 Jan 2026 04:50:50 +0800 Subject: [PATCH 22/31] Fix uMesh RF POWER configuration error (#9326) * fix issue https://github.com/linser233/uMesh/issues/1 * fix issue https://github.com/linser233/uMesh/issues/1 * Update and rename lora-usb-umesh-1262.yaml to lora-usb-umesh-1262-30dbm.yaml * Update and rename lora-usb-umesh-1268.yaml to lora-usb-umesh-1268-30dbm.yaml --- bin/config.d/lora-usb-umesh-1262-30dbm.yaml | 23 +++++++++++++++++++++ bin/config.d/lora-usb-umesh-1262.yaml | 15 -------------- bin/config.d/lora-usb-umesh-1268-30dbm.yaml | 23 +++++++++++++++++++++ bin/config.d/lora-usb-umesh-1268.yaml | 15 -------------- 4 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 bin/config.d/lora-usb-umesh-1262-30dbm.yaml delete mode 100644 bin/config.d/lora-usb-umesh-1262.yaml create mode 100644 bin/config.d/lora-usb-umesh-1268-30dbm.yaml delete mode 100644 bin/config.d/lora-usb-umesh-1268.yaml diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml new file mode 100644 index 000000000..7726eccd1 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -0,0 +1,23 @@ +Lora: + Module: sx1262 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 22 +# Reduce output power to improve EMI + NUM_PA_POINTS: 22 + TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 +# Note: This module integrates an additional PA to achieve higher output power. +# The 'power' parameter here does not represent the actual RF output. +# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). +# Each array element corresponds to the additional gain when that input level is set, +# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. +# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. diff --git a/bin/config.d/lora-usb-umesh-1262.yaml b/bin/config.d/lora-usb-umesh-1262.yaml deleted file mode 100644 index 6008e63b7..000000000 --- a/bin/config.d/lora-usb-umesh-1262.yaml +++ /dev/null @@ -1,15 +0,0 @@ -Lora: - Module: sx1262 - CS: 0 - IRQ: 6 - Reset: 1 - Busy: 4 - RXen: 2 - DIO2_AS_RF_SWITCH: true - spidev: ch341 - USB_PID: 0x5512 - USB_VID: 0x1A86 - DIO3_TCXO_VOLTAGE: true -# USB_Serialnum: 12345678 - SX126X_MAX_POWER: 30 -# Reduce output power to improve EMI diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml new file mode 100644 index 000000000..c054a92f9 --- /dev/null +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -0,0 +1,23 @@ +Lora: + Module: sx1268 + CS: 0 + IRQ: 6 + Reset: 1 + Busy: 4 + RXen: 2 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + USB_PID: 0x5512 + USB_VID: 0x1A86 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + SX126X_MAX_POWER: 22 +# Reduce output power to improve EMI + NUM_PA_POINTS: 22 + TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 +# Note: This module integrates an additional PA to achieve higher output power. +# The 'power' parameter here does not represent the actual RF output. +# TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). +# Each array element corresponds to the additional gain when that input level is set, +# The effective RF output is: Pout ≈ Pset + TX_GAIN_LORA[index]. +# Please refer to https://github.com/linser233/uMesh/blob/main/RF_Power.md for detailed information. diff --git a/bin/config.d/lora-usb-umesh-1268.yaml b/bin/config.d/lora-usb-umesh-1268.yaml deleted file mode 100644 index 637472966..000000000 --- a/bin/config.d/lora-usb-umesh-1268.yaml +++ /dev/null @@ -1,15 +0,0 @@ -Lora: - Module: sx1268 - CS: 0 - IRQ: 6 - Reset: 1 - Busy: 4 - RXen: 2 - DIO2_AS_RF_SWITCH: true - spidev: ch341 - USB_PID: 0x5512 - USB_VID: 0x1A86 - DIO3_TCXO_VOLTAGE: true -# USB_Serialnum: 12345678 - SX126X_MAX_POWER: 30 -# Reduce output power to improve EMI From fd498bebad72f6e69b2bf65ea6b9057f62667a48 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 27 Jan 2026 16:09:18 -0600 Subject: [PATCH 23/31] Add support for Hackaday Communicator function keys (#9444) --- src/graphics/Screen.cpp | 20 ++++++++++++++++++++ src/input/HackadayCommunicatorKeyboard.cpp | 13 +++++++------ src/input/InputBroker.h | 5 +++++ src/input/TCA8418KeyboardBase.h | 7 ++++++- src/input/kbI2cBase.cpp | 20 ++++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8bf69b7a0..111a47f7c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1731,6 +1731,26 @@ int Screen::handleInputEvent(const InputEvent *event) showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { showFrame(FrameDirection::NEXT); + } else if (event->inputEvent == INPUT_BROKER_FN_F1) { + this->ui->switchToFrame(0); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F2) { + this->ui->switchToFrame(1); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F3) { + this->ui->switchToFrame(2); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F4) { + this->ui->switchToFrame(3); + lastScreenTransition = millis(); + setFastFramerate(); + } else if (event->inputEvent == INPUT_BROKER_FN_F5) { + this->ui->switchToFrame(4); + lastScreenTransition = millis(); + setFastFramerate(); } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { // Long press up button for fast frame switching showPrevFrame(); diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index 87c8a24ae..c6a9e0ae8 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -20,20 +20,20 @@ constexpr uint8_t modifierLeftShift = 0b0001; // Num chars per key, Modulus for rotating through characters static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = { - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0, }; static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, - {}, + {Key::FUNCTION_F1}, {'+'}, {'9'}, {'8'}, {'7'}, - {'2'}, - {'3'}, - {'4'}, - {'5'}, + {Key::FUNCTION_F2}, + {Key::FUNCTION_F3}, + {Key::FUNCTION_F4}, + {Key::FUNCTION_F5}, {Key::ESC}, {'q', 'Q'}, {'w', 'W'}, @@ -141,6 +141,7 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key) if (state == Init || state == Busy) { return; } + LOG_DEBUG("Key pressed: %u", key); if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { modifierFlag = 0; diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index c55d7fa53..970e9969a 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -27,6 +27,11 @@ enum input_broker_event { INPUT_BROKER_SHUTDOWN = 0x9b, INPUT_BROKER_GPS_TOGGLE = 0x9e, INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_FN_F1 = 0xf1, + INPUT_BROKER_FN_F2 = 0xf2, + INPUT_BROKER_FN_F3 = 0xf3, + INPUT_BROKER_FN_F4 = 0xf4, + INPUT_BROKER_FN_F5 = 0xf5, INPUT_BROKER_MATRIXKEY = 0xFE, INPUT_BROKER_ANYKEY = 0xff diff --git a/src/input/TCA8418KeyboardBase.h b/src/input/TCA8418KeyboardBase.h index 8e509ac7e..e608c6da5 100644 --- a/src/input/TCA8418KeyboardBase.h +++ b/src/input/TCA8418KeyboardBase.h @@ -26,7 +26,12 @@ class TCA8418KeyboardBase GPS_TOGGLE = 0x9E, MUTE_TOGGLE = 0xAC, SEND_PING = 0xAF, - BL_TOGGLE = 0xAB + BL_TOGGLE = 0xAB, + FUNCTION_F1 = 0xF1, + FUNCTION_F2 = 0xF2, + FUNCTION_F3 = 0xF3, + FUNCTION_F4 = 0xF4, + FUNCTION_F5 = 0xF5 }; typedef uint8_t (*i2c_com_fptr_t)(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 12d0822f6..d744ee2ca 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -321,6 +321,26 @@ int32_t KbI2cBase::runOnce() e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_TAB; break; + case TCA8418KeyboardBase::FUNCTION_F1: + e.inputEvent = INPUT_BROKER_FN_F1; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F2: + e.inputEvent = INPUT_BROKER_FN_F2; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F3: + e.inputEvent = INPUT_BROKER_FN_F3; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F4: + e.inputEvent = INPUT_BROKER_FN_F4; + e.kbchar = 0x00; + break; + case TCA8418KeyboardBase::FUNCTION_F5: + e.inputEvent = INPUT_BROKER_FN_F5; + e.kbchar = 0x00; + break; default: if (nextEvent > 127) { e.inputEvent = INPUT_BROKER_NONE; From 69a42e1fd2a65e34ca87cd2e9c9f8c3d89ccf627 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 27 Jan 2026 18:00:20 -0600 Subject: [PATCH 24/31] Add portduino_status, assign hardware device IDs... (#9441) * Add portduino_status, assign hardware device IDs, and try to recover a CH341 device on a USB error * Minor fixes suggested by Copilot --- src/main.cpp | 39 ++++++++++++++++- src/mesh/NodeDB.cpp | 7 ++- src/mesh/SX126xInterface.cpp | 16 ++++++- src/platform/portduino/PortduinoGlue.cpp | 11 +++++ src/platform/portduino/PortduinoGlue.h | 9 +++- src/platform/portduino/USBHal.h | 54 ++++++++++++++++++++++-- src/platform/portduino/architecture.h | 2 +- variants/native/portduino.ini | 2 +- 8 files changed, 129 insertions(+), 11 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 93cc89296..3ccc06afb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1401,7 +1401,43 @@ void loop() if (inputBroker) inputBroker->processInputEventQueue(); #endif -#if ARCH_PORTDUINO && HAS_TFT +#if ARCH_PORTDUINO + if (portduino_config.lora_spi_dev == "ch341" && ch341Hal != nullptr) { + ch341Hal->checkError(); + } + if (portduino_status.LoRa_in_error && rebootAtMsec == 0) { + LOG_ERROR("LoRa in error detected, attempting to recover"); + if (rIf != nullptr) { + delete rIf; + rIf = nullptr; + } + if (portduino_config.lora_spi_dev == "ch341") { + if (ch341Hal != nullptr) { + delete ch341Hal; + ch341Hal = nullptr; + sleep(3); + } + try { + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); + } catch (std::exception &e) { + std::cerr << e.what() << std::endl; + std::cerr << "Could not initialize CH341 device!" << std::endl; + exit(EXIT_FAILURE); + } + } + if (initLoRa()) { + router->addInterface(rIf); + portduino_status.LoRa_in_error = false; + } else { + LOG_WARN("Reconfigure failed, rebooting"); + if (screen) { + screen->showSimpleBanner("Rebooting..."); + } + rebootAtMsec = millis() + 25; + } + } +#if HAS_TFT if (screen && portduino_config.displayPanel == x11 && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { auto dispdev = screen->getDisplayDevice(); @@ -1409,6 +1445,7 @@ void loop() static_cast(dispdev)->sdlLoop(); } #endif +#endif #if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE messageStoreAutosaveTick(); #endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ca6d49ca2..30d1633d5 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -2247,7 +2247,10 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened #ifdef ARCH_PORTDUINO - LOG_ERROR("A critical failure occurred, portduino is exiting"); - exit(2); + LOG_ERROR("A critical failure occurred"); + // TODO: Determine if other critical errors should also cause an immediate exit + if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE || + code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE) + exit(2); #endif } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 498496a3b..9dfc46bee 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -269,8 +269,12 @@ template void SX126xInterface::setStandby() if (err != RADIOLIB_ERR_NONE) LOG_DEBUG("SX126x standby %s%d", radioLibErr, err); +#ifdef ARCH_PORTDUINO + if (err != RADIOLIB_ERR_NONE) + portduino_status.LoRa_in_error = true; +#else assert(err == RADIOLIB_ERR_NONE); - +#endif isReceiving = false; // If we were receiving, not any more activeReceiveStart = 0; disableInterrupt(); @@ -313,7 +317,12 @@ template void SX126xInterface::startReceive() int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X startReceiveDutyCycleAuto %s%d", radioLibErr, err); +#ifdef ARCH_PORTDUINO + if (err != RADIOLIB_ERR_NONE) + portduino_status.LoRa_in_error = true; +#else assert(err == RADIOLIB_ERR_NONE); +#endif RadioLibInterface::startReceive(); @@ -341,7 +350,12 @@ template bool SX126xInterface::isChannelActive() return true; if (result != RADIOLIB_CHANNEL_FREE) LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result); +#ifdef ARCH_PORTDUINO + if (result == RADIOLIB_ERR_WRONG_MODEM) + portduino_status.LoRa_in_error = true; +#else assert(result != RADIOLIB_ERR_WRONG_MODEM); +#endif return false; } diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 9159d5954..d0d8ba40f 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,7 @@ #include "platform/portduino/USBHal.h" portduino_config_struct portduino_config; +portduino_status_struct portduino_status; std::ofstream traceFile; std::ofstream JSONFile; Ch341Hal *ch341Hal = nullptr; @@ -400,6 +402,11 @@ void portduinoSetup() if (found_hat) { product_config = cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); + if (strncmp(hat_vendor, "RAK", strlen("RAK")) == 0 && + strncmp(autoconf_product, "6421 Pi Hat", strlen("6421 Pi Hat")) == 0) { + std::cout << "autoconf: Setting hardwareModel to RAK6421" << std::endl; + portduino_status.hardwareModel = meshtastic_HardwareModel_RAK6421; + } } else if (found_ch341) { product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); // look for more data after the null terminator @@ -408,6 +415,10 @@ void portduinoSetup() memcpy(portduino_config.device_id, autoconf_product + len + 1, 16); if (!memfll(portduino_config.device_id, '\0', 16) && !memfll(portduino_config.device_id, 0xff, 16)) { portduino_config.has_device_id = true; + if (strncmp(autoconf_product, "MESHSTICK 1262", strlen("MESHSTICK 1262")) == 0) { + std::cout << "autoconf: Setting hardwareModel to Meshstick 1262" << std::endl; + portduino_status.hardwareModel = meshtastic_HardwareModel_MESHSTICK_1262; + } } } } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index eb4eb8925..af511be6e 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -1,15 +1,22 @@ #pragma once #include #include +#include #include #include #include "LR11x0Interface.h" #include "Module.h" #include "mesh/generated/meshtastic/mesh.pb.h" -#include "platform/portduino/USBHal.h" #include "yaml-cpp/yaml.h" +extern struct portduino_status_struct { + bool LoRa_in_error = false; + _meshtastic_HardwareModel hardwareModel = meshtastic_HardwareModel_PORTDUINO; +} portduino_status; + +#include "platform/portduino/USBHal.h" + // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index ecc292430..441f75b10 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -9,6 +9,8 @@ #include #include +extern uint32_t rebootAtMsec; + // include the library for Raspberry GPIO pins #define PI_RISING (PINEDIO_INT_MODE_RISING) @@ -45,7 +47,7 @@ class Ch341Hal : public RadioLibHal int32_t ret = pinedio_init(&pinedio, NULL); if (ret != 0) { std::string s = "Could not open SPI: "; - throw(s + std::to_string(ret)); + throw std::runtime_error(s + std::to_string(ret)); } pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); @@ -74,30 +76,55 @@ class Ch341Hal : public RadioLibHal // RADIOLIB_NC as an alias for non-connected pins void pinMode(uint32_t pin, uint32_t mode) override { + if (checkError()) { + return; + } if (pin == RADIOLIB_NC) { return; } - pinedio_set_pin_mode(&pinedio, pin, mode); + auto res = pinedio_set_pin_mode(&pinedio, pin, mode); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal pinMode: Could not set pin %u mode to %u: %d", pin, mode, res); + } } void digitalWrite(uint32_t pin, uint32_t value) override { + if (checkError()) { + return; + } if (pin == RADIOLIB_NC) { return; } - pinedio_digital_write(&pinedio, pin, value); + auto res = pinedio_digital_write(&pinedio, pin, value); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal digitalWrite: Could not write pin %u: %d", pin, res); + portduino_status.LoRa_in_error = true; + } } uint32_t digitalRead(uint32_t pin) override { + if (checkError()) { + return 0; + } if (pin == RADIOLIB_NC) { return 0; } - return pinedio_digital_read(&pinedio, pin); + auto res = pinedio_digital_read(&pinedio, pin); + if (res < 0 && rebootAtMsec == 0) { + LOG_ERROR("USBHal digitalRead: Could not read pin %u: %d", pin, res); + portduino_status.LoRa_in_error = true; + return 0; + } + return res; } void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { + if (checkError()) { + return; + } if (interruptNum == RADIOLIB_NC) { return; } @@ -107,6 +134,9 @@ class Ch341Hal : public RadioLibHal void detachInterrupt(uint32_t interruptNum) override { + if (checkError()) { + return; + } if (interruptNum == RADIOLIB_NC) { return; } @@ -152,6 +182,9 @@ class Ch341Hal : public RadioLibHal void spiTransfer(uint8_t *out, size_t len, uint8_t *in) { + if (checkError()) { + return; + } int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); if (ret < 0) { std::cerr << "Could not perform SPI transfer: " << ret << std::endl; @@ -160,9 +193,22 @@ class Ch341Hal : public RadioLibHal void spiEndTransaction() {} void spiEnd() {} + bool checkError() + { + if (pinedio.in_error) { + if (!has_warned) + LOG_ERROR("USBHal: libch341 in_error detected"); + portduino_status.LoRa_in_error = true; + has_warned = true; + return true; + } + has_warned = false; + return false; + } private: pinedio_inst pinedio = {0}; + bool has_warned = false; }; #endif diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index e10519d21..9ee8ad366 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -6,7 +6,7 @@ // set HW_VENDOR // -#define HW_VENDOR meshtastic_HardwareModel_PORTDUINO +#define HW_VENDOR portduino_status.hardwareModel #ifndef HAS_BUTTON #define HAS_BUTTON 1 diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index cc6c39aa2..55905481a 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -29,7 +29,7 @@ lib_deps = # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.7 # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main - https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip + https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main From 4eb4c4b584f529fd55c796bbb6aae9d2377fd470 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 27 Jan 2026 20:11:56 -0600 Subject: [PATCH 25/31] BaseUI Message Bubble Improvements (#9452) * Improve Message bubbles for more distinct markers and improved layout * Tune message bubble size and corner markers * Finish message bubble tuning --------- Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> --- src/graphics/draw/MessageRenderer.cpp | 83 +++++++++------------------ 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 72746e415..193164439 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -431,45 +431,6 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i return iconTop + tallest - 1; } -static void drawRoundedRectOutline(OLEDDisplay *display, int x, int y, int w, int h, int r) -{ - if (w <= 1 || h <= 1) - return; - - if (r < 0) - r = 0; - - int maxR = (std::min(w, h) / 2) - 1; - if (r > maxR) - r = maxR; - - if (r == 0) { - display->drawRect(x, y, w, h); - return; - } - - const int x0 = x; - const int y0 = y; - const int x1 = x + w - 1; - const int y1 = y + h - 1; - - // sides - if (x0 + r <= x1 - r) { - display->drawLine(x0 + r, y0, x1 - r, y0); // top - display->drawLine(x0 + r, y1, x1 - r, y1); // bottom - } - if (y0 + r <= y1 - r) { - display->drawLine(x0, y0 + r, x0, y1 - r); // left - display->drawLine(x1, y0 + r, x1, y1 - r); // right - } - - // corner arcs - display->drawCircleQuads(x0 + r, y0 + r, r, 2); // top left - display->drawCircleQuads(x1 - r, y0 + r, r, 1); // top right - display->drawCircleQuads(x1 - r, y1 - r, r, 8); // bottom right - display->drawCircleQuads(x0 + r, y1 - r, r, 4); // bottom left -} - static std::vector buildMessageBlocks(const std::vector &isHeaderVec, const std::vector &isMineVec) { std::vector blocks; @@ -909,27 +870,37 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 bubbleW = std::max(1, rightEdge - bubbleX); if (bubbleW > 1 && bubbleH > 1) { - int r = BUBBLE_RADIUS; - int maxR = (std::min(bubbleW, bubbleH) / 2) - 1; - if (maxR < 0) - maxR = 0; - if (r > maxR) - r = maxR; - - drawRoundedRectOutline(display, bubbleX, topY, bubbleW, bubbleH, r); - const int extra = 3; - const int rr = r + extra; int x1 = bubbleX + bubbleW - 1; int y1 = topY + bubbleH - 1; - if (!b.mine) { - // top-left corner square - display->drawLine(bubbleX, topY, bubbleX + rr, topY); - display->drawLine(bubbleX, topY, bubbleX, topY + rr); + if (b.mine) { + // Send Message (Right side) + display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH); + // Top Right Corner + display->drawRect(x1, topY, 2, 1); + display->drawRect(x1, topY, 1, 2); + // Bottom Right Corner + display->drawRect(x1 - 1, bottomY - 2, 2, 1); + display->drawRect(x1, bottomY - 3, 1, 2); + // Knock the corners off to make a bubble + display->setColor(BLACK); + display->drawRect(x1 - bubbleW, topY - 1, 1, 1); + display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1); + display->setColor(WHITE); } else { - // bottom-right corner square - display->drawLine(x1 - rr, y1, x1, y1); - display->drawLine(x1, y1 - rr, x1, y1); + // Received Message (Left Side) + display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH); + // Top Left Corner + display->drawRect(bubbleX + 1, topY + 1, 2, 1); + display->drawRect(bubbleX + 1, topY + 1, 1, 2); + // Bottom Left Corner + display->drawRect(bubbleX + 1, bottomY - 1, 2, 1); + display->drawRect(bubbleX + 1, bottomY - 2, 1, 2); + // Knock the corners off to make a bubble + display->setColor(BLACK); + display->drawRect(bubbleX + bubbleW, topY, 1, 1); + display->drawRect(bubbleX + bubbleW, bottomY, 1, 1); + display->setColor(WHITE); } } } From d0562e1ee657dec366751a10ff8b6955b2b12d13 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 28 Jan 2026 10:50:56 -0600 Subject: [PATCH 26/31] Add model workflows (#9462) * Add GitHub workflows for issue completeness, duplicate detection, onboarding, and contribution quality checks * Fix indentation * Refactor GitHub workflows for issue handling * Consolidate to two triage workflows * Update .github/workflows/models_pr_triage.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/models_issue_triage.yml | 204 ++++++++++++++++++++++ .github/workflows/models_pr_triage.yml | 138 +++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 .github/workflows/models_issue_triage.yml create mode 100644 .github/workflows/models_pr_triage.yml diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml new file mode 100644 index 000000000..f61a15fe6 --- /dev/null +++ b/.github/workflows/models_issue_triage.yml @@ -0,0 +1,204 @@ +name: Issue Triage (Models) + +on: + issues: + types: [opened] + +permissions: + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub issue spam, AI-generated slop, or low quality? + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + // Set output to skip remaining steps + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Duplicate detection - only if not spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect duplicate issues + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Completeness check + auto-labeling (combined into one AI call) + # ───────────────────────────────────────────────────────────────────────── + - name: Determine if completeness check should be skipped + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: actions/github-script@v8 + id: check-skip + with: + script: | + const title = (context.payload.issue.title || '').toLowerCase(); + const labels = (context.payload.issue.labels || []).map(label => label.name); + const hasFeatureRequest = title.includes('feature request'); + const hasEnhancement = labels.includes('enhancement'); + const shouldSkip = hasFeatureRequest && hasEnhancement; + core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); + + - name: Analyze issue completeness and determine labels + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' + uses: actions/ai-inference@v2 + id: analysis + continue-on-error: true + with: + prompt: | + Analyze this GitHub issue for completeness and determine if it needs labels. + + If this looks like a bug on the device/firmware (crash, reboot, lockup, radio issues, GPS issues, display issues, power/sleep issues), request device logs and explain how to get them: + + Web Flasher logs: + - Go to https://flasher.meshtastic.org + - Connect the device via USB and click Connect + - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs + + Meshtastic CLI logs: + - Run: meshtastic --port --noproto + - Reproduce the problem, then copy/paste the terminal output + + Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. + + Respond ONLY with JSON: + { + "complete": true|false, + "comment": "Your helpful comment requesting missing info, or empty string if complete", + "label": "needs-logs" | "needs-info" | "none" + } + + Use "needs-logs" if this is a device bug AND no logs are attached. + Use "needs-info" if basic info like firmware version or steps to reproduce are missing. + Use "none" if the issue is complete or is a feature request. + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. + model: openai/gpt-4o-mini + + - name: Process analysis result + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' + uses: actions/github-script@v8 + id: process + env: + AI_RESPONSE: ${{ steps.analysis.outputs.response }} + with: + script: | + const raw = (process.env.AI_RESPONSE || '').trim(); + + let complete = false; + let comment = ''; + let label = 'none'; + + try { + const parsed = JSON.parse(raw); + complete = !!parsed.complete; + comment = (parsed.comment ?? '').toString().trim(); + label = (parsed.label ?? 'none').toString().trim().toLowerCase(); + } catch { + // If JSON parse fails, treat as incomplete with raw response as comment + complete = false; + comment = raw; + label = 'none'; + } + + // Validate label + const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); + if (!allowedLabels.has(label)) label = 'none'; + + core.setOutput('should_comment', (!complete && comment.length > 0) ? 'true' : 'false'); + core.setOutput('comment_body', comment); + core.setOutput('label', label); + + - name: Apply triage label + if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' + uses: actions/github-script@v8 + env: + LABEL_NAME: ${{ steps.process.outputs.label }} + with: + script: | + const label = process.env.LABEL_NAME; + const labelMeta = { + 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, + 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + - name: Comment on issue + if: steps.process.outputs.should_comment == 'true' + uses: actions/github-script@v8 + env: + COMMENT_BODY: ${{ steps.process.outputs.comment_body }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: process.env.COMMENT_BODY + }); diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml new file mode 100644 index 000000000..ef303c02a --- /dev/null +++ b/.github/workflows/models_pr_triage.yml @@ -0,0 +1,138 @@ +name: PR Triage (Models) + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Check if PR already has automation/type labels (skip if so) + # ───────────────────────────────────────────────────────────────────────── + - name: Check existing labels + uses: actions/github-script@v8 + id: check-labels + with: + script: | + const skipLabels = new Set(['automation']); + const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); + const prLabels = context.payload.pull_request.labels.map(l => l.name); + + const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); + const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); + + core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); + core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Quality check (spam/AI-slop detection) + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + if: steps.check-labels.outputs.skip_all != 'true' + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub pull request spam, AI-generated slop, or low quality? + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v8 + id: quality-label + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); + + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) + # ───────────────────────────────────────────────────────────────────────── + - name: Classify PR for labeling + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') + uses: actions/ai-inference@v2 + id: classify + continue-on-error: true + with: + max-tokens: 30 + prompt: | + Classify this pull request into exactly one category. + + Return exactly one of: bugfix, hardware-support, enhancement + + Use bugfix if it fixes a bug, crash, or incorrect behavior. + Use hardware-support if it adds or improves support for a specific hardware device/variant. + Use enhancement if it adds a new feature, improves performance, or refactors code. + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. + model: openai/gpt-4o-mini + + - name: Apply type label + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' + uses: actions/github-script@v8 + env: + TYPE_LABEL: ${{ steps.classify.outputs.response }} + with: + script: | + const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, + 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, + 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); From c1e3f56324eeb2d64c2818ee6377af697575fbc0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:08:07 -0600 Subject: [PATCH 27/31] Update LovyanGFX to v1.2.19 (#9405) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- variants/esp32/chatter2/platformio.ini | 2 +- variants/esp32/m5stack_core/platformio.ini | 2 +- variants/esp32/wiphone/platformio.ini | 2 +- variants/esp32s3/heltec_v4/platformio.ini | 2 +- variants/esp32s3/heltec_wireless_tracker/platformio.ini | 2 +- variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini | 2 +- variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini | 2 +- variants/esp32s3/mesh-tab/platformio.ini | 2 +- variants/esp32s3/picomputer-s3/platformio.ini | 2 +- variants/esp32s3/rak_wismesh_tap_v2/platformio.ini | 2 +- variants/esp32s3/t-deck/platformio.ini | 2 +- variants/esp32s3/t-watch-s3/platformio.ini | 2 +- variants/esp32s3/tlora-pager/platformio.ini | 2 +- variants/esp32s3/tracksenger/platformio.ini | 4 ++-- variants/esp32s3/unphone/platformio.ini | 2 +- variants/native/portduino.ini | 2 +- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/variants/esp32/chatter2/platformio.ini b/variants/esp32/chatter2/platformio.ini index 4218e8503..a14e407a1 100644 --- a/variants/esp32/chatter2/platformio.ini +++ b/variants/esp32/chatter2/platformio.ini @@ -12,4 +12,4 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/m5stack_core/platformio.ini b/variants/esp32/m5stack_core/platformio.ini index 4544d73b5..5e41d5f2b 100644 --- a/variants/esp32/m5stack_core/platformio.ini +++ b/variants/esp32/m5stack_core/platformio.ini @@ -36,4 +36,4 @@ lib_ignore = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/wiphone/platformio.ini b/variants/esp32/wiphone/platformio.ini index fcf36a23c..fbd77be75 100644 --- a/variants/esp32/wiphone/platformio.ini +++ b/variants/esp32/wiphone/platformio.ini @@ -11,7 +11,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander sparkfun/SX1509 IO Expander@3.0.6 # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 4495a409f..20b3b4606 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -126,7 +126,7 @@ build_flags = lib_deps = ${heltec_v4_base.lib_deps} ; ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip ; TODO revert to official device-ui (when merged) diff --git a/variants/esp32s3/heltec_wireless_tracker/platformio.ini b/variants/esp32s3/heltec_wireless_tracker/platformio.ini index c2dab0c93..33643c541 100644 --- a/variants/esp32s3/heltec_wireless_tracker/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker/platformio.ini @@ -24,4 +24,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini index efeb63b8e..ab6592afb 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini @@ -22,4 +22,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 0b486618b..a5277ba19 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -20,4 +20,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index 21d8fb432..ecf5498b3 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -51,7 +51,7 @@ lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [mesh_tab_xpt2046] extends = mesh_tab_base diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index bef7b19a0..82147f222 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -24,7 +24,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 build_src_filter = ${esp32s3_base.build_src_filter} diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index e4ff9812c..8423bb4df 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -16,7 +16,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [ft5x06] extends = mesh_tab_base diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 58335796a..5a96a3839 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -24,7 +24,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 9c7a642b2..8c79ca097 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -22,7 +22,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index ea6369f29..cf8be67fb 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -29,7 +29,7 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM diff --git a/variants/esp32s3/tracksenger/platformio.ini b/variants/esp32s3/tracksenger/platformio.ini index 419a3539b..c006cf835 100644 --- a/variants/esp32s3/tracksenger/platformio.ini +++ b/variants/esp32s3/tracksenger/platformio.ini @@ -22,7 +22,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-lcd] custom_meshtastic_hw_model = 48 @@ -48,7 +48,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-oled] custom_meshtastic_hw_model = 48 diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index 28be1f3e1..924dfa74f 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -37,7 +37,7 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 + lovyan03/LovyanGFX@1.2.19 # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index cc6c39aa2..f99ee9a6f 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -27,7 +27,7 @@ lib_deps = # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library From a922751afc1b87a4e8c21ff94440a221e580233c Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 28 Jan 2026 11:12:02 -0600 Subject: [PATCH 28/31] External Notification - handleReceived Rewrite (#9454) * First steps in consolidating code and minimizing rewrite * Continuing code cleanup * Merge containsBell and !isMuted to a single code path * Forgot about alert_message_buzzer in the cleanup * More code refinements and cleanup * Fix nagCycleCutoff * CoPilot Updates --- src/modules/ExternalNotificationModule.cpp | 126 +++++++-------------- 1 file changed, 38 insertions(+), 88 deletions(-) diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 8b7ce700a..5e6985bdf 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -442,6 +442,7 @@ ExternalNotificationModule::ExternalNotificationModule() ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { + // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { #ifdef T_WATCH_S3 drv.setWaveform(0, 75); @@ -456,6 +457,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP for (size_t i = 0; i < p.payload.size; i++) { if (p.payload.bytes[i] == ASCII_BELL) { containsBell = true; + break; } } @@ -465,90 +467,47 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP // If we receive a broadcast message, apply channel mute setting // If we receive a direct message and the receipent is us, apply DM mute setting // Else we just handle it as not muted. - const bool directToUs = !isBroadcast(mp.to) && isToUs(&mp); - bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) - : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); + const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); + bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) + : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); - if (moduleConfig.external_notification.alert_bell) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell"); + const bool buzzerModeIsDirectOnly = + (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); + + if (containsBell || !is_muted) { + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || + moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_message_vibra || + ((moduleConfig.external_notification.alert_bell_buzzer || + moduleConfig.external_notification.alert_message_buzzer) && + canBuzz())) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); isNagging = true; + } + + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { + LOG_INFO("externalNotificationModule - Notification Module or Bell"); setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } } - } - if (moduleConfig.external_notification.alert_bell_vibra) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); - isNagging = true; + if (moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_message_vibra) { + LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } + + if ((moduleConfig.external_notification.alert_bell_buzzer || + moduleConfig.external_notification.alert_message_buzzer) && + canBuzz()) { + LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); - isNagging = true; - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } - - if (moduleConfig.external_notification.alert_message && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_vibra && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - - if (moduleConfig.external_notification.alert_message_buzzer && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (!isBroadcast(mp.to) && isToUs(&mp))) { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us - isNagging = true; + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us #ifdef T_LORA_PAGER - if (canBuzz()) { drv.setWaveform(0, 16); // Long buzzer 100% drv.setWaveform(1, 0); // Pause drv.setWaveform(2, 16); @@ -558,11 +517,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP drv.setWaveform(6, 16); drv.setWaveform(7, 0); drv.go(); - } #endif - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { #ifdef HAS_I2S if (moduleConfig.external_notification.use_i2s_as_buzzer) { audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); @@ -570,18 +525,13 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP #endif if (moduleConfig.external_notification.use_pwm) { rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } else { - // Don't beep if buzzer mode is "direct messages only" and it is no direct message - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } } + setIntervalFromNow(0); // run once so we know if we should do something } } else { From 571c1ac34c19461740236a1906b82e8c87f35f84 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 28 Jan 2026 14:08:32 -0600 Subject: [PATCH 29/31] Initial serialModule cleanup (#9465) * Initial serialModule cleanup * Move SERIAL_PRINT_PORT definition to variant.h * Add missed c6 check * Update src/modules/SerialModule.cpp Compile error for invalid SERIAL_PRINT_OBJECT value Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/SerialModule.cpp | 53 ++++++++----------- variants/esp32c6/m5stack_unitc6l/variant.h | 2 + variants/esp32c6/tlora_c6/variant.h | 2 + .../esp32s3/ELECROW-ThinkNode-M5/variant.h | 2 + .../nrf52840/ELECROW-ThinkNode-M1/variant.h | 2 + .../nrf52840/ELECROW-ThinkNode-M3/variant.h | 2 + .../nrf52840/ELECROW-ThinkNode-M4/variant.h | 2 + variants/nrf52840/canaryone/variant.h | 2 + variants/nrf52840/heltec_mesh_solar/variant.h | 2 + variants/nrf52840/meshlink/variant.h | 1 + variants/nrf52840/muzi_base/variant.h | 2 + variants/nrf52840/t-echo-lite/variant.h | 2 + variants/nrf52840/t-echo-plus/variant.h | 2 + variants/nrf52840/t-echo/variant.h | 1 + variants/stm32/CDEBYTE_E77-MBL/variant.h | 2 + variants/stm32/rak3172/variant.h | 1 + 16 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 5699f3be6..7a969343e 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -63,29 +63,26 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ - defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \ - defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) - -SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial; -#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL) -SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial1; -#else -SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial2; +#ifndef SERIAL_PRINT_PORT +#define SERIAL_PRINT_PORT 2 #endif +#if SERIAL_PRINT_PORT == 0 +#define SERIAL_PRINT_OBJECT Serial +#elif SERIAL_PRINT_PORT == 1 +#define SERIAL_PRINT_OBJECT Serial1 +#elif SERIAL_PRINT_PORT == 2 +#define SERIAL_PRINT_OBJECT Serial2 +#else +#error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2." +#endif + +SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} +static Print *serialPrint = &SERIAL_PRINT_OBJECT; + char serialBytes[512]; size_t serialPayloadSize; @@ -205,9 +202,7 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } -#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#elif SERIAL_PRINT_PORT != 0 if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 @@ -264,9 +259,7 @@ int32_t SerialModule::runOnce() } } -#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -540,11 +533,7 @@ ParsedLine parseLine(const char *line) */ void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && \ - !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h index d973aa281..1654ee590 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.h +++ b/variants/esp32c6/m5stack_unitc6l/variant.h @@ -50,3 +50,5 @@ void c6l_init(); #endif #define SCREEN_TRANSITION_FRAMERATE 10 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32c6/tlora_c6/variant.h b/variants/esp32c6/tlora_c6/variant.h index 55635fe13..4a0d40232 100644 --- a/variants/esp32c6/tlora_c6/variant.h +++ b/variants/esp32c6/tlora_c6/variant.h @@ -19,3 +19,5 @@ #define SX126X_TXEN 14 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 77a64f717..353741d91 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -81,4 +81,6 @@ #define BUTTON_PIN PIN_BUTTON1 #define BUTTON_PIN_ALT PIN_BUTTON2 + +#define SERIAL_PRINT_PORT 0 #endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index cde0f49c1..6f7056d27 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -159,6 +159,8 @@ External serial flash WP25R1635FZUIL0 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN +#define SERIAL_PRINT_PORT 0 + /* * SPI Interfaces */ diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index 29a6c85fd..decdd6ed0 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -113,6 +113,8 @@ extern "C" { #define LR11X0_DIO3_TCXO_VOLTAGE 3.3 #define LR11X0_DIO_AS_RF_SWITCH +#define SERIAL_PRINT_PORT 0 + // PCF8563 RTC Module // REVISIT https://github.com/meshtastic/firmware/pull/9084 // #define PCF8563_RTC 0x51 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h index faca5b075..1aff32838 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -135,6 +135,8 @@ static const uint8_t A0 = PIN_A0; #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 61d1e8df9..537f4e26c 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -170,6 +170,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 112bcd8b3..c80b81da4 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -142,6 +142,8 @@ No longer populated on PCB #define BQ4050_SCL_PIN (32 + 0) // I2C clock line pin #define BQ4050_EMERGENCY_SHUTDOWN_PIN (32 + 3) // Emergency shutdown pin +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/meshlink/variant.h b/variants/nrf52840/meshlink/variant.h index d1dba574f..2e252ff95 100644 --- a/variants/nrf52840/meshlink/variant.h +++ b/variants/nrf52840/meshlink/variant.h @@ -54,6 +54,7 @@ extern "C" { */ #define PIN_SERIAL1_RX (32 + 8) #define PIN_SERIAL1_TX (7) +#define SERIAL_PRINT_PORT 0 /* * SPI Interfaces diff --git a/variants/nrf52840/muzi_base/variant.h b/variants/nrf52840/muzi_base/variant.h index 96604c400..8a3532b77 100644 --- a/variants/nrf52840/muzi_base/variant.h +++ b/variants/nrf52840/muzi_base/variant.h @@ -176,6 +176,8 @@ extern "C" { #define EXTERNAL_FLASH_DEVICES W25Q32JVSS #define EXTERNAL_FLASH_USE_QSPI +#define SERIAL_PRINT_PORT 0 + // NFC is disabled via CONFIG_NFCT_PINS_AS_GPIOS=1 build flag // This configures P0.09 and P0.10 as regular GPIO pins instead of NFC pins diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h index 0748f6d48..12dcb8949 100644 --- a/variants/nrf52840/t-echo-lite/variant.h +++ b/variants/nrf52840/t-echo-lite/variant.h @@ -171,6 +171,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + // #define NO_EXT_GPIO 1 // PINs back side // Batt & solar connector left up corner diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h index 226f6d6fe..2d00f8c1f 100644 --- a/variants/nrf52840/t-echo-plus/variant.h +++ b/variants/nrf52840/t-echo-plus/variant.h @@ -138,6 +138,8 @@ static const uint8_t A0 = PIN_A0; // Battery / ADC already defined above #define HAS_RTC 1 +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index 9244fc6c3..8e72fd84b 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -88,6 +88,7 @@ static const uint8_t A0 = PIN_A0; /* * Serial interfaces */ +#define SERIAL_PRINT_PORT 0 /* No longer populated on PCB diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h index e3d111a33..2cf355a61 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/variant.h +++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h @@ -19,5 +19,7 @@ Do not expect a working Meshtastic device with this target. // #define LED_PIN PB3 // LED2 #define LED_STATE_ON 1 +#define SERIAL_PRINT_PORT 1 + #define EBYTE_E77_MBL #endif diff --git a/variants/stm32/rak3172/variant.h b/variants/stm32/rak3172/variant.h index 30d2b57b4..b3f6cbcda 100644 --- a/variants/stm32/rak3172/variant.h +++ b/variants/stm32/rak3172/variant.h @@ -17,5 +17,6 @@ Do not expect a working Meshtastic device with this target. #define LED_STATE_ON 1 #define RAK3172 +#define SERIAL_PRINT_PORT 1 #endif From f710cd6ecbd415d9e1c1fc9ec9a028f4b0bd3003 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 28 Jan 2026 12:14:01 -0800 Subject: [PATCH 30/31] Support fully direct request/responses (#9455) Co-authored-by: Ben Meadors --- src/mesh/ReliableRouter.cpp | 6 ------ src/mesh/Router.cpp | 7 +++++++ src/modules/RoutingModule.cpp | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 2b9b17183..42c24c783 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -17,12 +17,6 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) { if (p->want_ack) { - // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our - // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop - // counts and we want this message to get through the whole mesh, so use the default. - if (p->hop_limit == 0) { - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - } DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("ReliableRouter::send", copy); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 1a3270040..287fbcf60 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -266,6 +266,13 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) } } + // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our + // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop + // counts and we want this message to get through the whole mesh, so use the default. + if (src == RX_SRC_USER && p->want_ack && p->hop_limit == 0) { + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + } + return send(p); } } diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index e9e1fc786..d87cf3a44 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -67,6 +67,8 @@ uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif + } else if (mp.hop_start == 0) { + return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path. } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } From 1d219a93abd2b819619d7918dc7bad912be687d9 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 28 Jan 2026 14:58:05 -0600 Subject: [PATCH 31/31] Move input init to an init function in InputBroker (#9463) * Move input init to an init function in iInputBroker * Unbreak targets with EXCLUDE_INPUTBROKER * Update src/input/InputBroker.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix conditional compilation for input broker * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Trunk --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/input/InputBroker.cpp | 310 ++++++++++++++++++++++++++++++++++++++ src/input/InputBroker.h | 5 +- src/main.cpp | 226 +-------------------------- src/modules/Modules.cpp | 76 ---------- 4 files changed, 317 insertions(+), 300 deletions(-) diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 0aa78e2b8..c0a56233f 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -1,8 +1,59 @@ #include "InputBroker.h" #include "PowerFSM.h" // needed for event trigger #include "configuration.h" +#include "graphics/Screen.h" #include "modules/ExternalNotificationModule.h" +#if ARCH_PORTDUINO +#include "input/LinuxInputImpl.h" +#include "input/SeesawRotary.h" +#include "platform/portduino/PortduinoGlue.h" +#endif + +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/ExpressLRSFiveWay.h" +#include "input/RotaryEncoderImpl.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/SerialKeyboardImpl.h" +#include "input/UpDownInterruptImpl1.h" +#include "input/i2cButton.h" +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif + +#include "modules/StatusLEDModule.h" + +#if !MESHTASTIC_EXCLUDE_I2C +#include "input/cardKbI2cImpl.h" +#endif +#include "input/kbMatrixImpl.h" +#endif + +#if HAS_BUTTON || defined(ARCH_PORTDUINO) +#include "input/ButtonThread.h" + +#if defined(BUTTON_PIN_TOUCH) +ButtonThread *TouchButtonThread = nullptr; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) +static bool touchBacklightWasOn = false; +static bool touchBacklightActive = false; +#endif +#endif + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) +ButtonThread *UserButtonThread = nullptr; +#endif + +#if defined(ALT_BUTTON_PIN) +ButtonThread *BackButtonThread = nullptr; +#endif + +#if defined(CANCEL_BUTTON_PIN) +ButtonThread *CancelButtonThread = nullptr; +#endif + +#endif + InputBroker *inputBroker = nullptr; InputBroker::InputBroker() @@ -74,3 +125,262 @@ void InputBroker::pollSoonWorker(void *p) vTaskDelete(NULL); } #endif + +void InputBroker::Init() +{ + +#ifdef BUTTON_PIN +#ifdef ARCH_ESP32 + +#if ESP_ARDUINO_VERSION_MAJOR >= 3 +#ifdef BUTTON_NEED_PULLUP + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#endif +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#ifdef BUTTON_NEED_PULLUP + gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + delay(10); +#endif +#ifdef BUTTON_NEED_PULLUP2 + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); +#endif +#endif +#endif +#endif + +// buttons are now inputBroker, so have to come after setupModules +#if HAS_BUTTON + int pullup_sense = 0; +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did +#ifdef BUTTON_SENSE_TYPE + pullup_sense = BUTTON_SENSE_TYPE; +#else + pullup_sense = INPUT_PULLUP_SENSE; +#endif +#endif +#if defined(ARCH_PORTDUINO) + + if (portduino_config.userButtonPin.enabled) { + + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } + } +#endif + +#ifdef BUTTON_PIN_TOUCH + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + TouchButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; +#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) + // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds + touchConfig.longPress = INPUT_BROKER_NONE; + touchConfig.suppressLeadUpSound = true; + touchConfig.onPress = []() { + touchBacklightWasOn = uiconfig.screen_brightness == 1; + if (!touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, HIGH); + } + touchBacklightActive = true; + }; + touchConfig.onRelease = []() { + if (touchBacklightActive && !touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, LOW); + } + touchBacklightActive = false; + }; +#endif + TouchButtonThread->initButton(touchConfig); +#endif + +#if defined(CANCEL_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + CancelButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); +#endif + +#if defined(ALT_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + BackButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); +#endif + +#if defined(BUTTON_PIN) +#if defined(USERPREFS_BUTTON_PIN) + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; +#else + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; +#endif +#ifndef BUTTON_ACTIVE_LOW +#define BUTTON_ACTIVE_LOW true +#endif +#ifndef BUTTON_ACTIVE_PULLUP +#define BUTTON_ACTIVE_PULLUP true +#endif + + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_NONE; + userConfigNoScreen.longPressTime = 500; + userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } +#endif +#endif + +#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { +#if defined(T_LORA_PAGER) + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } +#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } +#else + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } +#endif + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); +#if defined(M5STACK_UNITC6L) + i2cButton = new i2cButtonThread("i2cButtonThread"); +#endif +#ifdef INPUTBROKER_MATRIX_TYPE + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE +#ifdef INPUTBROKER_SERIAL_TYPE + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE + } +#endif // HAS_BUTTON +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } +#endif +#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } +#endif +#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE + expressLRSFiveWayInput = new ExpressLRSFiveWay(); +#endif +} diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 970e9969a..9fcdd845f 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -1,6 +1,7 @@ #pragma once #include "Observer.h" +#include "concurrency/OSThread.h" #include "freertosinc.h" #ifdef InputBrokerDebug @@ -76,6 +77,7 @@ class InputBroker : public Observable void queueInputEvent(const InputEvent *event); void processInputEventQueue(); #endif + void Init(); protected: int handleInputEvent(const InputEvent *event); @@ -89,4 +91,5 @@ class InputBroker : public Observable #endif }; -extern InputBroker *inputBroker; \ No newline at end of file +extern InputBroker *inputBroker; +extern bool runASAP; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 3ccc06afb..68eda2d0d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,31 +120,6 @@ void printPartitionTable() #endif // DEBUG_PARTITION_TABLE #endif // ARCH_ESP32 -#if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "input/ButtonThread.h" - -#if defined(BUTTON_PIN_TOUCH) -ButtonThread *TouchButtonThread = nullptr; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) -static bool touchBacklightWasOn = false; -static bool touchBacklightActive = false; -#endif -#endif - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) -ButtonThread *UserButtonThread = nullptr; -#endif - -#if defined(ALT_BUTTON_PIN) -ButtonThread *BackButtonThread = nullptr; -#endif - -#if defined(CANCEL_BUTTON_PIN) -ButtonThread *CancelButtonThread = nullptr; -#endif - -#endif - #include "AmbientLightingThread.h" #include "PowerFSMThread.h" @@ -509,30 +484,6 @@ void setup() LOG_INFO("Wait for peripherals to stabilize"); delay(PERIPHERAL_WARMUP_MS); #endif - -#ifdef BUTTON_PIN -#ifdef ARCH_ESP32 - -#if ESP_ARDUINO_VERSION_MAJOR >= 3 -#ifdef BUTTON_NEED_PULLUP - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#endif -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - delay(10); -#endif -#ifdef BUTTON_NEED_PULLUP2 - gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); - delay(10); -#endif -#endif -#endif -#endif - initSPI(); OSThread::setup(); @@ -999,180 +950,9 @@ void setup() nodeDB->hasWarned = true; } #endif - -// buttons are now inputBroker, so have to come after setupModules -#if HAS_BUTTON - int pullup_sense = 0; -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did -#ifdef BUTTON_SENSE_TYPE - pullup_sense = BUTTON_SENSE_TYPE; -#else - pullup_sense = INPUT_PULLUP_SENSE; -#endif -#endif -#if defined(ARCH_PORTDUINO) - - if (portduino_config.userButtonPin.enabled) { - - LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig config; - config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; - config.activeLow = true; - config.activePullup = true; - config.pullupSense = INPUT_PULLUP; - config.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - config.singlePress = INPUT_BROKER_USER_PRESS; - config.longPress = INPUT_BROKER_SELECT; - UserButtonThread->initButton(config); - } - } -#endif - -#ifdef BUTTON_PIN_TOUCH - TouchButtonThread = new ButtonThread("BackButton"); - ButtonConfig touchConfig; - touchConfig.pinNumber = BUTTON_PIN_TOUCH; - touchConfig.activeLow = true; - touchConfig.activePullup = true; - touchConfig.pullupSense = pullup_sense; - touchConfig.intRoutine = []() { - TouchButtonThread->userButton.tick(); - TouchButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - touchConfig.singlePress = INPUT_BROKER_NONE; - touchConfig.longPress = INPUT_BROKER_BACK; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) - // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds - touchConfig.longPress = INPUT_BROKER_NONE; - touchConfig.suppressLeadUpSound = true; - touchConfig.onPress = []() { - touchBacklightWasOn = uiconfig.screen_brightness == 1; - if (!touchBacklightWasOn) { - digitalWrite(PIN_EINK_EN, HIGH); - } - touchBacklightActive = true; - }; - touchConfig.onRelease = []() { - if (touchBacklightActive && !touchBacklightWasOn) { - digitalWrite(PIN_EINK_EN, LOW); - } - touchBacklightActive = false; - }; -#endif - TouchButtonThread->initButton(touchConfig); -#endif - -#if defined(CANCEL_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - CancelButtonThread = new ButtonThread("CancelButton"); - ButtonConfig cancelConfig; - cancelConfig.pinNumber = CANCEL_BUTTON_PIN; - cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; - cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; - cancelConfig.pullupSense = pullup_sense; - cancelConfig.intRoutine = []() { - CancelButtonThread->userButton.tick(); - CancelButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - cancelConfig.singlePress = INPUT_BROKER_CANCEL; - cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; - cancelConfig.longPressTime = 4000; - CancelButtonThread->initButton(cancelConfig); -#endif - -#if defined(ALT_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - BackButtonThread = new ButtonThread("BackButton"); - ButtonConfig backConfig; - backConfig.pinNumber = ALT_BUTTON_PIN; - backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; - backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; - backConfig.pullupSense = pullup_sense; - backConfig.intRoutine = []() { - BackButtonThread->userButton.tick(); - BackButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - backConfig.singlePress = INPUT_BROKER_ALT_PRESS; - backConfig.longPress = INPUT_BROKER_ALT_LONG; - backConfig.longPressTime = 500; - BackButtonThread->initButton(backConfig); -#endif - -#if defined(BUTTON_PIN) -#if defined(USERPREFS_BUTTON_PIN) - int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; -#else - int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; -#endif -#ifndef BUTTON_ACTIVE_LOW -#define BUTTON_ACTIVE_LOW true -#endif -#ifndef BUTTON_ACTIVE_PULLUP -#define BUTTON_ACTIVE_PULLUP true -#endif - - // Buttons. Moved here cause we need NodeDB to be initialized - // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig userConfig; - userConfig.pinNumber = (uint8_t)_pinNum; - userConfig.activeLow = BUTTON_ACTIVE_LOW; - userConfig.activePullup = BUTTON_ACTIVE_PULLUP; - userConfig.pullupSense = pullup_sense; - userConfig.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; - userConfig.longPress = INPUT_BROKER_SELECT; - userConfig.longPressTime = 500; - userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; - UserButtonThread->initButton(userConfig); - } else { - ButtonConfig userConfigNoScreen; - userConfigNoScreen.pinNumber = (uint8_t)_pinNum; - userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; - userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; - userConfigNoScreen.pullupSense = pullup_sense; - userConfigNoScreen.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; - userConfigNoScreen.longPress = INPUT_BROKER_NONE; - userConfigNoScreen.longPressTime = 500; - userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; - userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; - userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; - UserButtonThread->initButton(userConfigNoScreen); - } -#endif - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER + if (inputBroker) + inputBroker->Init(); #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index e17868baf..c3463a07e 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,24 +1,7 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "buzz/BuzzerFeedbackThread.h" -#include "input/ExpressLRSFiveWay.h" -#include "input/InputBroker.h" -#include "input/RotaryEncoderImpl.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/SerialKeyboardImpl.h" -#include "input/UpDownInterruptImpl1.h" -#include "input/i2cButton.h" #include "modules/SystemCommandsModule.h" -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" -#endif - -#include "modules/StatusLEDModule.h" - -#if !MESHTASTIC_EXCLUDE_I2C -#include "input/cardKbI2cImpl.h" -#endif -#include "input/kbMatrixImpl.h" #endif #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.h" @@ -59,8 +42,6 @@ #include "modules/WaypointModule.h" #endif #if ARCH_PORTDUINO -#include "input/LinuxInputImpl.h" -#include "input/SeesawRotary.h" #include "modules/Telemetry/HostMetrics.h" #if !MESHTASTIC_EXCLUDE_STOREFORWARD #include "modules/StoreForwardModule.h" @@ -179,63 +160,6 @@ void setupModules() #endif // Example: Put your module here // new ReplyModule(); -#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#if defined(T_LORA_PAGER) - // use a special FSM based rotary encoder version for T-LoRa Pager - rotaryEncoderImpl = new RotaryEncoderImpl(); - if (!rotaryEncoderImpl->init()) { - delete rotaryEncoderImpl; - rotaryEncoderImpl = nullptr; - } -#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } -#else - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } -#endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); -#if defined(M5STACK_UNITC6L) - i2cButton = new i2cButtonThread("i2cButtonThread"); -#endif -#ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE -#ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE - } -#endif // HAS_BUTTON -#if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; - } - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); - } -#endif -#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); - } -#endif -#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE - expressLRSFiveWayInput = new ExpressLRSFiveWay(); -#endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { cannedMessageModule = new CannedMessageModule();